ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Dependency Injection Patterns

Decouple components using dependency injection for testability and flexibility. Covers constructor injection, service locators, DI containers, interface segregation, composition root, and the patterns that make code modular without overengineering.

Dependency injection (DI) is the most impactful design pattern for testable, maintainable code. Instead of components creating their own dependencies, they receive them from the outside. This single change — inverting the direction of dependency creation — makes code testable, swappable, and modular.


The Problem

# WITHOUT DI: Hard-coded dependencies (untestable)
class OrderService:
    def __init__(self):
        self.db = PostgresDatabase("prod-connection-string")  # Hard-coded!
        self.payment = StripeClient("sk_live_xxx")              # Hard-coded!
        self.email = SendGridClient("api-key-xxx")              # Hard-coded!
    
    def place_order(self, order):
        self.db.save(order)
        self.payment.charge(order.total)
        self.email.send_confirmation(order.email)

# How do you test this without charging a credit card?
# How do you test this without a real database?
# Answer: You can't. Dependencies are hard-coded.

Constructor Injection

# WITH DI: Dependencies passed in (testable, flexible)
class OrderService:
    def __init__(self, db, payment, email):
        self.db = db          # Could be Postgres, SQLite, or mock
        self.payment = payment # Could be Stripe, PayPal, or mock
        self.email = email     # Could be SendGrid, SES, or mock
    
    def place_order(self, order):
        self.db.save(order)
        self.payment.charge(order.total)
        self.email.send_confirmation(order.email)

# Production:
service = OrderService(
    db=PostgresDatabase("prod-connection-string"),
    payment=StripeClient("sk_live_xxx"),
    email=SendGridClient("api-key-xxx"),
)

# Test:
service = OrderService(
    db=InMemoryDatabase(),        # Fast, no external deps
    payment=MockPaymentClient(),   # No real charges
    email=MockEmailClient(),       # No real emails
)

# Switching providers:
service = OrderService(
    db=PostgresDatabase("prod-connection-string"),
    payment=PayPalClient("client-id"),  # Swapped Stripe → PayPal
    email=SESClient("region"),          # Swapped SendGrid → SES
)
# Zero changes to OrderService code!

Interface Segregation

from abc import ABC, abstractmethod

# Define interfaces (contracts) for dependencies
class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> str:
        """Returns transaction ID."""
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        pass

# Implementations
class StripeGateway(PaymentGateway):
    def charge(self, amount, currency):
        return stripe.Charge.create(amount=amount, currency=currency).id
    
    def refund(self, transaction_id):
        stripe.Refund.create(charge=transaction_id)
        return True

class MockGateway(PaymentGateway):
    def __init__(self):
        self.charges = []
    
    def charge(self, amount, currency):
        txn_id = f"mock_{len(self.charges)}"
        self.charges.append({"id": txn_id, "amount": amount})
        return txn_id
    
    def refund(self, transaction_id):
        return True

# OrderService depends on interface, not implementation
class OrderService:
    def __init__(self, payment: PaymentGateway):
        self.payment = payment  # Any PaymentGateway works

DI Container

# For larger applications: DI container manages wiring
from dependency_injector import containers, providers

class AppContainer(containers.DeclarativeContainer):
    config = providers.Configuration()
    
    # Infrastructure
    database = providers.Singleton(
        PostgresDatabase,
        connection_string=config.db_url,
    )
    
    # Services
    payment_gateway = providers.Factory(
        StripeGateway,
        api_key=config.stripe_key,
    )
    
    email_client = providers.Factory(
        SendGridClient,
        api_key=config.sendgrid_key,
    )
    
    order_service = providers.Factory(
        OrderService,
        db=database,
        payment=payment_gateway,
        email=email_client,
    )

# Usage:
container = AppContainer()
container.config.from_env("APP_")  # Load from environment

service = container.order_service()
# All dependencies automatically wired!

Anti-Patterns

Anti-PatternConsequenceFix
new inside constructorsCannot test, cannot swapPass dependencies as constructor args
Service Locator everywhereHidden dependencies, test confusionPrefer constructor injection
Inject everythingOver-engineered, 20 constructor paramsInject only things that vary (external deps)
No interfacesDepend on concrete implementationsInterface for every external dependency
God containerSingle container knows everythingModular containers per feature

Dependency injection is about one thing: making dependencies visible and swappable. If you can test your code without external services and swap implementations without changing business logic, you have good DI.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →