Hace un par de años revisé un PR de una feature que recibía un webhook, validaba el payload y guardaba un registro en la base de datos. Tarea de una tarde. El PR tenía catorce archivos. Una interfaz IPayloadValidator, otra IPayloadValidatorFactory, un IRepository<T> genérico, un IUnitOfWork, tres clases que implementaban esas interfaces con exactamente una instancia cada una en todo el codebase, y un contenedor de inyección de dependencias configurado para resolver todo esto en runtime.
Le pregunté al autor por qué tanta estructura para algo tan pequeño. La respuesta fue: "Single Responsibility, Open/Closed, Dependency Inversion". Y ahí quedó claro el problema. No había un análisis de qué iba a cambiar, ni de quién iba a extender qué. Había una checklist de principios SOLID aplicada como un ritual, sin preguntarse si el problema los necesitaba.
Ese PR terminó con más líneas de infraestructura que de lógica de negocio. Y cuando, seis meses después, hubo que cambiar algo real, el cambio afectó a los catorce archivos igual, porque las abstracciones estaban mal cortadas: anticipaban un tipo de cambio que nunca llegó y no protegían contra el que sí llegó.
Esto no es un caso aislado. Es un patrón que cualquier ingeniero senior reconoce apenas se lo describe: código que sigue la letra de SOLID y traiciona su espíritu. Los principios SOLID nacieron para resolver un problema real de diseño orientado a objetos, pero en la práctica se enseñan y se aplican como reglas absolutas, no como heurísticas que dependen del contexto. Y esa diferencia es la que separa un sistema sostenible de uno que parece profesional, pero es un dolor de cabeza.
Single responsibility no significa una clase por verbo
La interpretación más común y más dañina de Single Responsibility es "una clase debe hacer una sola cosa", entendida en el sentido más literal posible: una clase que valida, otra que transforma, otra que persiste. El resultado es un flujo de datos que salta entre siete objetos para hacer algo que cabía perfectamente en un método de veinte líneas.
La definición original de Robert Martin es otra: una clase debería tener una sola razón para cambiar, ligada a un actor o a una necesidad del negocio. Eso es distinto a "una sola acción". Un servicio que valida y persiste un pedido puede tener una sola razón para cambiar (las reglas de negocio del pedido) aunque técnicamente ejecute dos pasos. Separarlo en OrderValidator y OrderPersister no reduce el acoplamiento real, solo lo distribuye en más archivos.
El costo de esto no es estético. Cada capa extra es un salto de contexto para quien lee el código después. Cuando alguien nuevo entra a un servicio y tiene que abrir cinco archivos para entender un flujo lineal, no está ante un diseño limpio. Está frente a una indirecta disfrazada de buena práctica.
Open/closed como excusa para abstraer sin necesidad
Open/Closed dice que el código debería estar abierto a la extensión y cerrado a la modificación. Es un principio útil cuando ya se han visto dos o tres variaciones reales de un comportamiento y hace falta agregar una cuarta sin romper las anteriores. Es un principio nocivo cuando se aplica antes de que exista esa variación.
Esto tiene nombre: abstracción prematura. Kent Beck lo resume mejor que cualquier principio: hacé que funcione, hacelo bien, después hacelo rápido, y en el medio, no anticipes estructura que el problema no pidió. La regla práctica que funciona mejor que Open/Closed aplicado a ciegas es la regla de tres: la primera vez que se escribe algo, se escribe directamente. La segunda vez que se repite con una variación, se duplica sin culpa. La tercera vez, ahí sí, se extrae la abstracción, porque ya hay evidencia real de qué varía y qué no.
Interface segregation cuando la interfaz nunca tuvo un segundo implementador
El principio de segregación de interfaces establece que ninguna clase debería depender de métodos que no usa. Tiene sentido en sistemas grandes con múltiples consumidores de una misma interfaz con necesidades distintas. El problema es que se usa como justificación para crear una interfaz cada vez que existe una clase, aunque esa interfaz tenga un único implementador desde el día uno y probablemente para siempre.
Si existe PaymentGateway y se crea IPaymentGateway solo porque "es buena práctica tener interfaces", no se está segregando nada. Se está agregando un nivel de indirecta que ni el compilador necesita ni el humano agradece. En lenguajes con tipado estructural o con buen soporte de mocking a nivel de clase, esa interfaz no habilita ningún testing que no existiera ya. Solo hace que ir a definición en el editor lleve a un contrato vacío antes de llegar a la implementación real.
Interface Segregation importa cuando el dolor ya existe: cuando una clase implementa una interfaz de doce métodos y solo usa tres, forzada a stub-ear el resto con excepciones de "no implementado". Ahí sí, partir la interfaz en piezas coherentes es una mejora real. Crear la partición antes de que el dolor exista es resolver un problema que no está.
Dependency inversion no es sinónimo de inyectar todo
Dependency Inversion establece que los módulos de alto nivel no deberían depender de detalles de bajo nivel, sino de abstracciones. Es la base de arquitecturas hexagonales y de puertos y adaptadores, y en esos contextos es indispensable: un dominio que no depende de la base de datos ni del framework HTTP es un dominio que se puede testear y mover.
Pero en la práctica cotidiana, "dependency inversion" se convirtió en sinónimo de "todo se inyecta a través de una interfaz, siempre, sin excepción". Eso genera código en el que una función que llama a Math.random() termina detrás de un IRandomProvider inyectado por el contenedor, porque en algún lado alguien leyó que las dependencias externas deben abstraerse. El resultado es un grafo de dependencias imposible de seguir a simple vista, con más ceremonia de configuración que lógica real, para un sistema que, en la práctica, nunca va a cambiar de proveedor de números aleatorios.
La pregunta que separa la inversión útil de la inversión decorativa es simple: ¿existe hoy, o hay evidencia concreta de que va a existir, una segunda implementación real de esta dependencia? Si la respuesta es “tal vez algún día”, la abstracción es una apuesta, no un diseño. Y las apuestas de arquitectura casi siempre las paga otro equipo, años después, tratando de entender por qué hay tres capas entre el controlador y una llamada a una API externa.
Dónde SOLID sí paga la abstracción que exige
Nada de esto significa que SOLID esté mal concebido. Significa que su aplicación tiene costo, y ese costo solo se justifica en contextos específicos. Liskov Substitution, por ejemplo, es difícil de violar por accidente en el día a día, pero cuando se viola (una subclase que lanza excepciones en métodos que la clase base garantiza que funcionan) el bug que produce es de los más difíciles de rastrear, porque el compilador no lo detecta y el código "parece" correcto.
Dependency Inversion vale su costo cuando el módulo en cuestión tiene múltiples implementaciones reales y probadas: un sistema de pagos que soporta Stripe y Mercado Pago hoy, no "podría soportar" mañana. Ahí la interfaz no es decoración, es la única forma sana de que el dominio no sepa nada de ninguno de los dos proveedores.
Open/Closed vale su costo en el núcleo de un producto que cambia con frecuencia predecible: un motor de reglas de pricing, un sistema de permisos con roles que se agregan cada trimestre. Ahí sí conviene diseñar para extensión, porque la extensión no es hipotética, es la operación normal del sistema.
Single Responsibility, bien entendido (una razón de cambio, no una acción), es casi siempre gratis de aplicar y rara vez tiene contraindicaciones reales. El problema nunca fue ese principio, fue su malinterpretación como "atomizar todo".
Los principios SOLID son un vocabulario de criterio, no una checklist
Lo que separa a un ingeniero senior de alguien que memorizó SOLID para la entrevista no es saber recitar las cinco letras. Es saber cuándo el costo de la abstracción es menor que el de no tenerla, y cuándo es al revés. Eso no sale de un acrónimo, sale de haber visto sistemas fallar en ambas direcciones: por rígidos y por sobrediseñados.
Aplicar SOLID a rajatabla, sin evaluar si el problema concreto lo requiere, no es una disciplina de ingeniería. Es tercerizar el juicio a una regla mnemotécnica y llamarlo una buena práctica. Un code review que exige interfaces, inyección de dependencias y separación de capas en un caso que no las necesita no mejora el diseño. Lo está complicando el uso adecuado del vocabulario técnico.
SOLID sirve como lenguaje compartido para discutir trade-offs de diseño, y como conjunto de heurísticas para cuando el sistema ya mostró señales reales de necesitar flexibilidad. No sirve como sustituto del criterio, y cuando se usa así, el resultado no es mejor arquitectura. Es la misma complejidad de siempre, con mejores nombres.



