Aprende cómo la inyección de dependencias hace que el código sea más fácil de probar, refactorizar y extender. Explora patrones prácticos, ejemplos y errores comunes a evitar.

Dependency Injection (DI) es una idea simple: en lugar de que un fragmento de código cree las cosas que necesita, se las das desde fuera.
Esas “cosas que necesita” son sus dependencias—por ejemplo, una conexión a la base de datos, un servicio de pagos, un reloj, un logger o un emisor de correos. Si tu código construye esas dependencias por sí mismo, queda silenciosamente atado a cómo funcionan esas dependencias.
Piensa en una máquina de café en una oficina. Depende de agua, granos y electricidad.
DI es ese segundo enfoque: la “máquina de café” (tu clase/función) se centra en hacer café (su trabajo), mientras que los “insumos” (dependencias) los proporciona quien la configura.
DI no obliga a usar un framework específico, ni es lo mismo que un contenedor DI. Puedes hacer DI manualmente pasando dependencias como parámetros (o via constructores) y listo.
DI tampoco es “mocking”. Mockear es una forma de usar DI en pruebas, pero DI en sí es solo una decisión de diseño sobre dónde se crean las dependencias.
Cuando las dependencias se proporcionan desde fuera, tu código es más fácil de ejecutar en distintos contextos: producción, pruebas unitarias, demos y futuras funcionalidades.
Esa misma flexibilidad hace que los módulos sean más limpios: las piezas se pueden reemplazar sin reconfigurar todo el sistema. Como resultado, las pruebas son más rápidas y claras (porque puedes intercambiar sustitutos simples) y la base de código resulta más fácil de cambiar (porque las partes están menos entrelazadas).
El acoplamiento fuerte ocurre cuando una parte de tu código decide directamente qué otras partes debe usar. La forma más habitual es sencilla: llamar a new dentro de la lógica de negocio.
Imagina una función de checkout que hace new StripeClient() y new SmtpEmailSender() internamente. Al principio parece cómodo—todo lo que necesitas está ahí. Pero también ata el flujo de checkout a esas implementaciones concretas, a detalles de configuración e incluso a sus reglas de creación (claves de API, timeouts, comportamiento de red).
Ese acoplamiento es “oculto” porque no es evidente en la firma del método. La función parece que solo procesa un pedido, pero en secreto depende de pasarelas de pago, proveedores de correo y quizá de una conexión a base de datos.
Cuando las dependencias están codificadas, incluso cambios pequeños se propagan:
Las dependencias codificadas hacen que las pruebas unitarias ejecuten trabajo real: llamadas de red, I/O en disco, relojes, IDs aleatorios o recursos compartidos. Las pruebas se vuelven lentas porque no están aisladas, y frágiles porque los resultados dependen del tiempo, servicios externos o del orden de ejecución.
Si ves estos patrones, es probable que el acoplamiento fuerte ya te esté costando tiempo:
new salpicado “por todas partes” en la lógica centralLa Inyección de Dependencias soluciona esto haciendo las dependencias explícitas y reemplazables—sin reescribir las reglas de negocio cada vez que cambia el mundo.
Inversión de Control (IoC) es un cambio de responsabilidad: una clase debe centrarse en qué necesita hacer, no en cómo obtener las cosas que necesita.
Cuando una clase crea sus propias dependencias (por ejemplo, new EmailService() o abriendo una conexión a base de datos directamente), asume calladamente dos trabajos: lógica de negocio y configuración. Eso hace que la clase sea más difícil de cambiar, reutilizar y probar.
Con IoC, tu código depende de abstracciones—como interfaces o pequeños tipos “contrato”—en lugar de implementaciones específicas.
Por ejemplo, un CheckoutService no necesita saber si los pagos se procesan vía Stripe, PayPal o un procesador de pruebas falso. Solo necesita “algo que pueda cobrar una tarjeta”. Si CheckoutService acepta un IPaymentProcessor, puede trabajar con cualquier implementación que cumpla ese contrato.
Esto mantiene tu lógica central estable aunque las herramientas subyacentes cambien.
La parte práctica de IoC es mover la creación de dependencias fuera de la clase y pasarlas (a menudo mediante el constructor). Aquí es donde encaja la inyección de dependencias (DI): DI es una forma común de lograr IoC.
En lugar de:
Obtienes:
El resultado es flexibilidad: intercambiar comportamientos se convierte en una decisión de configuración, no en una reescritura.
Si las clases no crean sus dependencias, algo debe hacerlo. Ese “algo” es la composition root: el lugar donde tu aplicación se arma—típicamente el código de arranque.
La composition root es donde decides: “En producción usa RealPaymentProcessor; en pruebas usa FakePaymentProcessor.” Mantener este cableado en un solo sitio reduce sorpresas y mantiene al resto del código centrado.
IoC hace que las pruebas unitarias sean más sencillas porque puedes proveer dobles de prueba pequeños y rápidos en lugar de invocar redes o bases de datos reales.
También hace que los refactors sean más seguros: cuando las responsabilidades están separadas, cambiar una implementación raramente obliga a cambiar las clases que la usan—siempre que la abstracción se mantenga.
DI no es una técnica única—es un pequeño conjunto de maneras de “alimentar” a una clase con las cosas que depende (como un logger, cliente de BD o pasarela de pago). El estilo que elijas afecta la claridad, la testabilidad y la facilidad de uso indebido.
Con la inyección por constructor, las dependencias son requeridas para construir el objeto. Esa es la gran ventaja: no puedes olvidarlas por accidente.
Es la mejor opción cuando una dependencia:
La inyección por constructor suele producir el código más claro y las pruebas unitarias más directas, porque tu prueba puede pasar un falso o mock justo en el momento de creación.
A veces una dependencia solo se necesita para una operación—por ejemplo, un formateador temporal, una estrategia especial o un valor con alcance de petición.
En esos casos, pásala como parámetro del método. Esto mantiene el objeto más pequeño y evita “promover” una necesidad puntual a un campo permanente.
La inyección por setter puede ser conveniente cuando realmente no puedes proporcionar una dependencia al construir el objeto (algunos frameworks o caminos de código legacy). El compromiso es que puede ocultar requisitos: la clase parece usable aunque no esté totalmente configurada.
Eso a menudo conduce a sorpresas en tiempo de ejecución (“¿por qué esto está undefined?”) y hace las pruebas más frágiles porque la configuración es fácil de omitir.
Las pruebas unitarias son más útiles cuando son rápidas, repetibles y centradas en un comportamiento. En el momento en que una prueba “unitaria” depende de una BD real, llamada de red, sistema de archivos o reloj, tiende a ralentizarse y volverse frágil. Peor aún, las fallas dejan de ser informativas: ¿falló el código o falló el entorno?
Dependency Injection (DI) arregla esto permitiendo que tu código acepte desde fuera las cosas de las que depende (acceso a datos, clientes HTTP, proveedores de tiempo). En las pruebas puedes intercambiar esas dependencias por sustitutos ligeros.
Una BD real o una API añade tiempo de preparación y latencia. Con DI, puedes inyectar un repositorio en memoria o un cliente fake que devuelva respuestas preparadas al instante. Eso significa:
Sin DI, el código a menudo hace new() de sus propias dependencias, obligando a las pruebas a recorrer toda la pila. Con DI puedes inyectar:
Sin hacks ni switches globales—solo pasar una implementación distinta.
DI hace que la configuración sea explícita. En lugar de escarbar en archivos de configuración, cadenas de conexión o variables de entorno solo para pruebas, puedes leer un test y ver inmediatamente qué es real y qué está sustituido.
Un test típico amigable con DI se lee así:
Arrange: crea el servicio con un repositorio fake y un reloj stub
Act: llama al método
Assert: verifica el valor de retorno y/o las interacciones con el mock
Esa sencillez reduce el ruido y hace que los fallos sean más fáciles de diagnosticar—exactamente lo que quieres de tus pruebas unitarias.
Un test seam es una “apertura” deliberada en tu código donde puedes intercambiar un comportamiento por otro. En producción enchufas lo real. En pruebas enchufas un sustituto más seguro y rápido. DI es una de las formas más sencillas de crear estos seams sin trucos.
Los seams son útiles alrededor de partes difíciles de controlar en una prueba:
Si tu lógica de negocio llama a estas cosas directamente, las pruebas se vuelven frágiles: fallan por razones ajenas a tu lógica (problemas de red, diferencias de zona horaria, archivos faltantes) y son más difíciles de ejecutar rápidamente.
Un seam suele tomar la forma de una interfaz—o en lenguajes dinámicos, un contrato simple como “este objeto debe tener un método now()”. La idea clave es depender de lo que necesitas, no de dónde viene.
Por ejemplo, en lugar de llamar al reloj del sistema dentro de un servicio de pedidos, puedes depender de un Clock:
SystemClock.now()FakeClock.now() devuelve un tiempo fijoEl mismo patrón funciona para lecturas de archivos (FileStore), envío de correos (Mailer) o cobros (PaymentGateway). La lógica central permanece igual; solo cambia la implementación enchufada.
Cuando puedes intercambiar comportamiento a propósito:
Los seams bien colocados reducen la necesidad de mockear todo. En su lugar, obtienes unos pocos puntos de sustitución limpios que mantienen las pruebas rápidas, enfocadas y predecibles.
Modularidad es la idea de que tu software está construido a partir de partes independientes (módulos) con límites claros: cada módulo tiene una responsabilidad enfocada y una forma bien definida de interactuar con el resto.
La inyección de dependencias (DI) apoya esto haciendo esos límites explícitos. En lugar de que un módulo cree o busque todo lo que necesita, lo recibe desde fuera. Ese pequeño cambio reduce cuánto “conoce” un módulo sobre otro.
Cuando el código construye dependencias internamente (por ejemplo, haciendo new de un cliente de BD dentro de un servicio), el llamador y la dependencia quedan fuertemente atados. DI te anima a depender de una interfaz (o un contrato simple), no de una implementación concreta.
Eso significa que un módulo normalmente solo necesita saber:
PaymentGateway.charge())Como resultado, los módulos cambian menos en conjunto, porque los detalles internos dejan de filtrarse a través de los límites.
Una base de código modular debería permitirte intercambiar un componente sin reescribir a quienes lo usan. DI hace esto práctico:
En cada caso, los llamadores siguen usando el mismo contrato. El “cableado” cambia en un solo lugar (composition root), en vez de ediciones dispersas por todo el código.
Los límites de dependencia claros facilitan que equipos trabajen en paralelo. Un equipo puede desarrollar una nueva implementación detrás de una interfaz acordada mientras otro sigue desarrollando características que dependen de esa interfaz.
DI también facilita refactors incrementales: puedes extraer un módulo, inyectarlo y reemplazarlo gradualmente—sin necesitar una reescritura de golpe.
Ver DI en código hace que cale más rápido que cualquier definición. Aquí tienes un ejemplo mínimo de “antes y después” usando una funcionalidad de notificaciones.
Cuando una clase hace new internamente, decide qué implementación usar y cómo construirla.
class EmailService {
send(to, message) {
// talks to real SMTP provider
}
}
class WelcomeNotifier {
notify(user) {
const email = new EmailService();
email.send(user.email, "Welcome!");
}
}
Dolor en las pruebas: una prueba unitaria corre el riesgo de disparar comportamiento real de correo (o requiere stubs globales incómodos).
test("sends welcome email", () => {
const notifier = new WelcomeNotifier();
notifier.notify({ email: "[email protected]" });
// Hard to assert without patching EmailService globally
});
Ahora WelcomeNotifier acepta cualquier objeto que cumpla el comportamiento necesario.
class WelcomeNotifier {
constructor(emailService) {
this.emailService = emailService;
}
notify(user) {
this.emailService.send(user.email, "Welcome!");
}
}
La prueba se vuelve pequeña, rápida y explícita.
test("sends welcome email", () => {
const fakeEmail = { send: vi.fn() };
const notifier = new WelcomeNotifier(fakeEmail);
notifier.notify({ email: "[email protected]" });
expect(fakeEmail.send).toHaveBeenCalledWith("[email protected]", "Welcome!");
});
¿Quieres SMS más tarde? No tocas WelcomeNotifier. Solo pasas una implementación distinta:
const smsService = { send: (to, msg) => {/* SMS provider */} };
const notifier = new WelcomeNotifier(smsService);
Ese es el beneficio práctico: las pruebas dejan de pelearse con los detalles de construcción y el nuevo comportamiento se añade intercambiando dependencias en lugar de reescribir código existente.
DI puede ser tan simple como “pasar lo que necesitas a quien lo usa”. Eso es DI manual. Un contenedor DI es una herramienta que automatiza ese cableado. Ambos pueden ser buenas elecciones—la clave es elegir el nivel de automatización que encaje con tu app.
Con DI manual creas objetos tú mismo y pasas dependencias por constructores (o parámetros). Es sencillo:
El cableado manual también fuerza buenas prácticas de diseño. Si un objeto necesita siete dependencias, el dolor es inmediato—y suele ser una señal para dividir responsabilidades.
A medida que crecen los componentes, el cableado manual puede convertirse en un “plumbing” repetitivo. Un contenedor DI puede ayudar:
Los contenedores brillan en apps con límites claros y ciclos de vida definidos—apps web, servicios de larga ejecución o sistemas donde muchas features dependen de infraestructura compartida.
Un contenedor puede hacer que un diseño fuertemente acoplado parezca ordenado porque el cableado desaparece. Pero los problemas subyacentes permanecen:
Si añadir un contenedor hace el código menos legible, o los devs dejan de saber qué depende de qué, probablemente has ido demasiado lejos.
Empieza con DI manual para mantener la claridad mientras moldeas tus módulos. Añade un contenedor cuando el cableado sea repetitivo o la gestión del ciclo de vida sea complicada.
Una regla práctica: usa DI manual dentro de tu código core/negocio y (opcionalmente) un contenedor en el límite de la app (composition root) para ensamblarlo todo. Esto mantiene el diseño claro mientras reduces boilerplate cuando el proyecto crece.
DI puede facilitar pruebas y cambios—pero solo si se usa con disciplina. Aquí están las formas más comunes en que DI sale mal, y hábitos para mantenerlo útil.
Si una clase necesita una larga lista de dependencias, suele estar haciendo demasiado. Eso no es un fallo de DI—es DI que revela un olor de diseño.
Una regla práctica: si no puedes describir el trabajo de la clase en una frase, o el constructor sigue creciendo, considera dividir la clase, extraer un colaborador más pequeño o agrupar operaciones relacionadas tras una única interfaz (con cuidado—no crees servicios “dios”).
El patrón Service Locator suele verse como llamar container.get(Foo) dentro del código de negocio. Parece cómodo, pero hace las dependencias invisibles: no puedes saber lo que necesita una clase leyendo su constructor.
Las pruebas se vuelven más difíciles porque debes configurar estado global (el locator) en vez de suministrar un conjunto claro y local de fakes. Prefiere pasar dependencias explícitamente (la inyección por constructor es la más directa) para que las pruebas puedan construir el grafo con intención.
Los contenedores DI pueden fallar en tiempo de ejecución cuando:
Estos problemas son frustrantes porque aparecen solo cuando el cableado se ejecuta.
Mantén los constructores pequeños y enfocados. Si la lista de dependencias de una clase crece, trátalo como prompt para refactorizar.
Añade pruebas de integración para el cableado. Incluso una prueba ligera de la “composition root” que construya tu contenedor (o el cableado manual) puede detectar registros faltantes y ciclos temprano—antes de producción.
Finalmente, mantén la creación de objetos en un solo lugar (normalmente el startup/composition root) y evita llamadas al contenedor dentro de la lógica de negocio. Esa separación preserva el principal beneficio de DI: claridad sobre qué depende de qué.
DI es más fácil de adoptar si lo tratas como una serie de pequeños refactors de bajo riesgo. Empieza donde las pruebas son lentas o frágiles, y donde los cambios suelen propagarse por código no relacionado.
Busca dependencias que dificulten las pruebas o la comprensión:
Si una función no puede ejecutarse sin salir del proceso, suele ser un buen candidato.
new o se llame directamente.Este enfoque mantiene cada cambio revisable y te permite parar después de cualquier paso sin romper el sistema.
DI puede convertir el código en “todo depende de todo” si inyectas demasiado.
Una buena regla: inyecta capacidades, no detalles. Por ejemplo, inyecta Clock en lugar de “SystemTime + TimeZoneResolver + NtpClient”. Si una clase necesita cinco servicios no relacionados, quizá hace demasiado—considera dividir responsabilidades.
También evita pasar dependencias a través de múltiples capas “por si acaso”. Inyecta solo donde se usan; centraliza el cableado en un lugar.
Si usas un generador de código o un flujo para crear features rápidamente, DI resulta aún más valiosa porque preserva la estructura a medida que el proyecto crece. Por ejemplo, cuando equipos usan Koder.ai para crear frontends React, servicios Go y backends con PostgreSQL desde una especificación guiada por chat, mantener una composition root clara e interfaces amigables para DI ayuda a que el código generado siga siendo fácil de probar, refactorizar e intercambiar integraciones (email, pagos, almacenamiento) sin reescribir la lógica central.
La regla sigue siendo: mantén la creación de objetos y el cableado específico del entorno en el límite, y deja el código de negocio centrado en el comportamiento.
Deberías poder señalar mejoras concretas:
Si quieres un siguiente paso, documenta tu “composition root” y mantenla aburrida: un archivo que haga el cableado mientras el resto del código se concentra en comportamiento.
Dependency Injection (DI) significa que tu código recibe las cosas que necesita (base de datos, logger, reloj, cliente de pagos) desde fuera en lugar de crearlas internamente.
En la práctica, eso suele verse como pasar dependencias en un constructor o como parámetros de función para que sean explícitas y reemplazables.
Inversión de Control (IoC) es la idea más amplia: una clase debe centrarse en lo que hace, no en cómo obtiene a sus colaboradores.
DI es una técnica común para lograr IoC moviendo la creación de dependencias hacia fuera y pasando las dependencias como parámetros.
Si se crea una dependencia con new dentro de la lógica de negocio, esa dependencia se vuelve difícil de reemplazar.
Eso conduce a:
DI ayuda a que las pruebas sean rápidas y deterministas porque puedes inyectar dobles de prueba en lugar de usar sistemas externos reales.
Intercambios comunes:
Un contenedor DI es opcional. Empieza con DI manual (pasar dependencias explícitamente) cuando:
Considera un contenedor cuando el cableado sea repetitivo o necesites gestión de ciclo de vida (singleton/por petición).
Usa inyección por constructor cuando la dependencia sea necesaria para que el objeto funcione y se use en varios métodos.
Usa inyección por método/parámetro si solo se necesita para una llamada (por ejemplo, un valor con ámbito de petición, una estrategia puntual).
Evita inyección por setter/propiedad salvo que realmente necesites un cableado tardío; añade validación para fallar rápido si falta.
La composición raíz (composition root) es el lugar donde ensamblas la aplicación: creas implementaciones y las pasas a los servicios que las necesitan.
Mantenla cerca del arranque de la app (punto de entrada) para que el resto del código se centre en comportamiento y no en el cableado.
Un test seam es un punto deliberado donde se puede intercambiar comportamiento.
Buenos lugares para seams son preocupaciones difíciles de probar:
Clock.now())DI crea seams permitiendo inyectar una implementación alternativa en las pruebas.
Errores comunes:
container.get() dentro de la lógica de negocio oculta dependencias reales; prefiere parámetros explícitos.Usa una refactorización pequeña y repetible:
Repite para el siguiente seam; puedes parar en cualquier momento sin necesidad de una reescritura masiva.