No history yet

Advanced Dependency Management

Mastering Dependencies

At the heart of Hexagonal Architecture is a simple goal: protect the core logic of your application from the messy, ever-changing outside world. The primary way we achieve this is by carefully managing how different parts of our code depend on each other. When your business logic is directly tied to a specific database or a particular messaging queue, it becomes brittle. Changing the database becomes a major surgery, not a simple swap.

The solution is to flip the dependencies upside down. Instead of the core logic depending on concrete details, the details should depend on the core logic's definitions. This is known as the Dependency Inversion Principle (DIP).

Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions.

In our hexagon, the “high-level module” is the application core—the business rules. The “low-level modules” are the adapters that talk to databases, APIs, and user interfaces. The “abstractions” are the ports. The core defines a port (an interface) for what it needs, like a UserRepository with save and findById methods. It doesn't know or care how those methods are implemented. An adapter for PostgreSQL then comes along and provides a concrete implementation of that interface. The dependency points inward, from the adapter to the core.

Injecting Dependencies

The Dependency Inversion Principle tells us what to do. Dependency Injection (DI) is how we do it. Instead of an object creating its own dependencies, we 'inject' them from the outside. This small change has huge benefits. It decouples the component from the concrete implementation of its dependencies, making it more reusable and much easier to test. For testing, you can inject a mock or fake version of the dependency instead of a real one.

If a service needs a repository, don't let the service build the repository. Instead, build the repository somewhere else and pass it into the service.

Let's see a practical example. Here's a service that creates its own dependency. This is a tightly coupled design.

// BEFORE: Tight coupling

// The concrete implementation
class PostgresUserRepository {
  findUser(id: string) { /* ... connects to Postgres */ }
}

// The service creates its own dependency
class UserService {
  private userRepository: PostgresUserRepository;

  constructor() {
    this.userRepository = new PostgresUserRepository(); // Bad!
  }

  getUser(id: string) {
    return this.userRepository.findUser(id);
  }
}

Now, let's refactor this using DI. We'll define an interface (our port) and inject the dependency through the constructor. This is a decoupled design.

// AFTER: Decoupled with DI

// The Port (abstraction)
interface UserRepository {
  findUser(id: string): User;
}

// The Adapter (concrete implementation)
class PostgresUserRepository implements UserRepository {
  findUser(id: string): User { /* ... connects to Postgres */ }
}

// The service receives its dependency
class UserService {
  private userRepository: UserRepository;

  constructor(userRepository: UserRepository) {
    this.userRepository = userRepository; // Good!
  }

  getUser(id: string) {
    return this.userRepository.findUser(id);
  }
}

// At the edge of the application (the composition root):
const dbAdapter = new PostgresUserRepository();
const userService = new UserService(dbAdapter);

Notice how UserService now depends only on the UserRepository interface. It has no idea that a PostgreSQL database is being used. We could easily swap PostgresUserRepository with a MongoUserRepository or a InMemoryTestRepository without changing a single line of code in UserService.

By moving the responsibility of creating and managing dependencies out of the class that uses them, Dependency Injection promotes a better separation of concerns.

Managing External Systems

This pattern is how we manage all external dependencies, not just databases. Whether you're sending an email, processing a payment, or fetching data from a third-party API, the pattern is the same:

  1. Define a Port: In your application core, create an interface that describes the interaction you need (e.g., PaymentGateway, EmailSender).
  2. Implement an Adapter: Outside the core, write a concrete class that implements this interface using a specific tool or service (e.g., StripePaymentGateway, SendGridEmailSender).
  3. Inject the Adapter: At the outermost layer of your application, create an instance of your adapter and inject it into the core logic that needs it.

This strict separation ensures your domain logic remains pure and uncontaminated by implementation details. The adapters handle the messy work of translating between your domain's needs and the specific APIs of external systems.

Follow the Dependency Rule: Always point dependencies inward

By using dependency inversion and injection, you ensure all dependencies point towards the center of your hexagon. This protects your core logic, maximizes its testability, and gives you the flexibility to adapt and evolve your application's infrastructure over time.

Quiz Questions 1/5

What is the primary goal of Hexagonal Architecture?

Quiz Questions 2/5

According to the Dependency Inversion Principle, which statement best describes the flow of dependencies in Hexagonal Architecture?