In Domain-Driven Design (DDD), the aim is to craft a domain model that encapsulates business rules and behaviors while remaining free (or “ignorant”) of infrastructure concerns like database access. This separation ensures that your business logic—represented by entities, value objects, and aggregates—remains testable, maintainable, and expressive of the ubiquitous language.

The Pure Domain Model

Entities, Value Objects, and Aggregates

  • Entities are objects that have a distinct identity (often a unique identifier) that persists over time. They encapsulate not just data but also behavior.
  • Value Objects represent descriptive aspects of the domain with no conceptual identity. They are immutable and defined solely by their attributes.
  • Aggregates are clusters of domain objects (entities and value objects) treated as a single unit for data changes. An aggregate has a root (the aggregate root) that enforces invariants and governs consistency within the cluster.

For more details, check out Eric Evans’ book on DDD and this article by Vaughn Vernon.

Keeping Domain Logic Pure

A central tenet of DDD is that your domain model should be persistence-ignorant. This means that business rules—wherever possible—should be expressed in pure code (without side effects) so that you can focus on business behavior without mixing in infrastructure concerns like data access or messaging. Pure domain logic is easier to test and understand, and its evolution over time is less likely to be affected by changes in the underlying infrastructure.


Query and Repository Logic: Where Do They Belong?

Repositories in DDD

A repository is an abstraction used to retrieve and persist aggregate roots. In DDD:

  • Repository Interfaces: The contract for data access is defined in the domain layer. These interfaces express the operations needed by the domain (such as “find by ID” or “save an aggregate”) using language that fits the ubiquitous language of your domain.

  • Repository Implementations: The actual query logic, data mapping, and persistence details belong in the infrastructure layer. This separation ensures that your core domain remains decoupled from the specific technology used to store data.

For an excellent explanation of this separation, see Martin Fowler’s Repository Pattern.

Where Does Query Logic Belong?

In many DDD implementations, especially those that adopt Command–Query Responsibility Segregation (CQRS), the query logic is separated entirely from the domain model:

  • Write Model: Contains the domain model, responsible for enforcing business invariants and processing commands.
  • Read Model: Often structured as Data Transfer Objects (DTOs) or projections, it is optimized for querying and is usually maintained by a separate service or repository implementation.

This separation is useful when your queries become complex or when you need to optimize for performance and scalability. For more on CQRS and read/write separation, see Martin Fowler’s article on CQRS.


The Role of the Dependency Inversion Principle (DIP)

Understanding DIP

The Dependency Inversion Principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

In the context of DDD:

  • The domain layer (high-level module) defines the repository interfaces (abstractions).
  • The infrastructure layer (low-level module) provides concrete implementations that handle data access and query logic.

Thus, the domain remains independent of any specific persistence technology, making it more resilient to change.

For a more comprehensive look at DIP, check out the Wikipedia article on the Dependency Inversion Principle and Uncle Bob’s discussion on Clean Architecture.


Examples in Object-Oriented vs. Functional Programming

Object-Oriented Approach

In OO design, your domain model typically consists of mutable entities and aggregates. You define repository interfaces in the domain layer, and the implementations in the infrastructure layer handle data mapping. For instance:

// Domain layer: Repository interface
public interface ReservationRepository {
    Optional<Reservation> findById(String id);
    void save(Reservation reservation);
}
 
// Domain entity (pure business logic, persistence-ignorant)
public class Reservation {
    private String id;
    private LocalDateTime createdOn;
    // Business methods here...
}
 
// Infrastructure layer: Repository implementation
public class JpaReservationRepository implements ReservationRepository {
    // JPA or Spring Data dependency injected here
    @Override
    public Optional<Reservation> findById(String id) {
        // Actual query using JPA...
    }
    @Override
    public void save(Reservation reservation) {
        // Persist using JPA...
    }
}

This approach keeps the domain model free of persistence details while satisfying DIP—because the domain depends only on the ReservationRepository interface, not its concrete implementation.

Functional Programming Approach

In functional programming (FP), you typically model your domain with immutable data structures and pure functions. Persistence is handled by separate functions that act as “shells” around your pure domain logic. Mark Seemann’s article “The Functional Core, Imperative Shell” is an excellent resource that illustrates this separation. In FP, you might have:

  • Pure functions that transform data.
  • Impure functions (in an outer “shell”) that perform side effects like querying a database.

This clear separation allows the core business logic to be easily tested and reasoned about independently from side effects—a concept that aligns with DDD’s persistence ignorance but is even more natural in FP due to immutability.


Conclusion

In a pure DDD approach, query and repository logic should not be embedded in the domain classes that encapsulate business behavior. Instead, the domain layer defines repository interfaces (which, by DIP, are high-level abstractions) while the infrastructure layer implements these interfaces, handling the actual data access. This separation ensures that your domain model remains focused on business rules (via entities, value objects, and aggregates) and is decoupled from persistence and other infrastructure concerns.

Whether you choose an OO or FP approach, the guiding principle remains the same: keep your pure business logic isolated from side-effect–laden operations. For further reading, explore the following resources:

By adhering to these practices, you ensure that your domain model remains expressive, testable, and resilient against changes in the underlying infrastructure.