Un tech lead lo resumió así en un post-mortem interno: "Aprobamos ese PR porque cumplía SOLID. Nadie preguntó si el problema lo necesitaba." La frase describía exactamente lo que había pasado: una feature que recibía un webhook, validaba el payload y guardaba un registro terminó en catorce archivos, con una interfaz IPayloadValidator, otra IPayloadValidatorFactory, un IRepository<T> genérico, un IUnitOfWork, tres clases con exactamente una implementación cada una en todo el codebase, y un contenedor de inyección de dependencias configurado para resolver todo eso en runtime.
Cuando alguien le preguntó al autor por qué tanta estructura para algo tan pequeño, la respuesta fue: "Single Responsibility, Open/Closed, Dependency Inversion". 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".
Antes de agregar una interfaz o una capa de indirección, cuatro preguntas simples ahorran meses de deuda técnica:
- ¿Existe hoy una segunda implementación real de esta dependencia, o es un "tal vez algún día"?
- ¿La clase tiene una sola razón de cambio, o solo hace una sola acción?
- ¿Ya aparecieron tres variaciones reales del comportamiento, o estás anticipando la primera?
- ¿Esa interfaz tiene, o va a tener, un segundo implementador, o existe porque "es buena práctica"?
Si la mayoría de las respuestas es "no estoy seguro", la abstracción es una apuesta disfrazada de buena práctica. SOLID da vocabulario para discutir esas apuestas. No las hace automáticamente correctas.



