Domain‑Driven Design (DDD) is an approach to software development that emphasizes deep understanding of the business domain, clear language shared between technical and domain experts (the ubiquitous language), and modeling that directly reflects real-world business processes. At its heart, DDD separates the overall architecture into strategic and tactical elements.

Strategic Design vs. Tactical Design

  • Strategic Design addresses the overall structure of your system. It involves identifying bounded contexts, determining relationships between different parts of the system, and defining a ubiquitous language. Key strategic concepts include:

    • Bounded Contexts: Clear boundaries within which a particular domain model applies.
    • Ubiquitous Language: A shared language between domain experts and developers.
    • Context Mapping: Understanding and modeling how different bounded contexts interact.
  • Tactical Design focuses on the building blocks used to implement the domain model. This includes patterns that capture business rules and behavior. The most common tactical building blocks are:

    • Entities
    • Value Objects
    • Aggregates
    • Repositories
    • Domain Services
    • Factories

For a thorough overview of strategic DDD, refer to Eric Evans’ seminal book on Domain‑Driven Design and the DDD Community website.

Tactical Building Blocks of DDD

Entities

Definition:
Entities are objects defined by their unique identity rather than their attributes. Even if the data associated with an entity changes over time, its identity remains the same.

Example:
A customer in a sales system might be represented as an entity. Even if the customer’s address or phone number changes, the customer’s unique identifier (like a customer ID) remains constant.

Further Reading:

Value Objects

Definition:
Value objects are immutable objects that are defined solely by their attributes. They do not have a conceptual identity and can be replaced if their value changes.

Example:
An address or a monetary amount can be modeled as a value object. Two addresses with the same street, city, and zip code are considered equal.

Benefits:

  • Immutability makes them easier to reason about.
  • They simplify equality comparisons.

Further Reading:

Aggregates

Definition:
An aggregate is a cluster of associated objects (entities and value objects) that are treated as a single unit for data changes. The aggregate root is the only member of the aggregate that external objects are allowed to reference directly.

Purpose:
Aggregates help enforce consistency boundaries within your domain model. All changes to objects within an aggregate should go through the aggregate root, ensuring that business invariants are maintained.

Example:
An order aggregate might consist of an order entity (the aggregate root), along with line items and payment details. External code interacts only with the order entity to update the entire order.

Further Reading:

Repositories

Definition:
Repositories provide an abstraction over data storage. They encapsulate the logic required to retrieve and persist aggregate roots, offering a collection-like interface to the domain.

Role in DDD:

  • Abstraction: Repository interfaces are defined in the domain layer. This ensures that the domain model remains persistence‑ignorant.
  • Implementation: The actual query and persistence logic resides in the infrastructure layer. By following the Dependency Inversion Principle (DIP), the domain depends only on the repository abstraction, not on its concrete implementation.

Example:
Imagine a repository interface for orders:

public interface OrderRepository {
    Optional<Order> findById(String orderId);
    void save(Order order);
}

The domain layer uses this interface without caring whether the data is stored in a relational database, a NoSQL store, or another persistence mechanism.

Further Reading:


Handling Domain Logic in OO vs. Functional Programming

While Domain‑Driven Design is often discussed in the context of object‑oriented programming, many of its principles can also be applied in functional programming (FP):

  • Object‑Oriented (OO) Approach:
    The domain model is typically made up of mutable objects (entities) and encapsulates behavior through methods. Repository interfaces defined in the domain layer are implemented by concrete classes in the infrastructure layer. This separation supports the Dependency Inversion Principle and keeps the business logic free from persistence concerns.

  • Functional Programming (FP) Approach:
    In FP, you tend to model the domain using immutable data structures and pure functions. The core business logic is implemented as a set of pure functions (the “functional core”) while side effects like data access are isolated in an “imperative shell.” Mark Seemann’s article “The Functional Core, Imperative Shell” is a great resource on this topic.

Both paradigms share the goal of keeping your domain logic pure and testable while separating out infrastructure concerns, albeit through different techniques.


Conclusion

Domain‑Driven Design provides a rich vocabulary and set of patterns to build software that deeply reflects the business domain. By clearly separating strategic design (bounded contexts and ubiquitous language) from tactical patterns (entities, value objects, aggregates, and repositories), you can develop a model that is both expressive and maintainable. Moreover, by applying the Dependency Inversion Principle, you ensure that your domain model remains independent of persistence details, allowing for flexible infrastructure changes. Whether you work in an OO or FP paradigm, these principles help you create robust, testable, and adaptable systems.

Further Reading and References

By exploring these resources, you’ll gain a deeper understanding of how to apply both strategic and tactical DDD principles in your own projects.