Domain-Driven Design (DDD) Guide

This guide provides an overview of key concepts and best practices for implementing Domain-Driven Design (DDD), focusing on the separation of concerns between the domain model, repository, and persistence layer.

Key Principles of DDD

  • Focus on the Domain:
    • The domain model represents the core business logic and rules.
    • It should be independent of technical concerns like persistence or frameworks.
  • Separation of Concerns:
    • Divide responsibilities between the domain layer, application layer, and infrastructure layer.
    • Keep the domain layer free from infrastructure dependencies.
  • Ubiquitous Language:
    • Use a shared language between developers and domain experts to ensure clarity and alignment.
  • Repository Pattern:
    • Use repositories to abstract persistence logic and provide access to domain objects.

Layers in DDD

[1]

com.example.todo
├── application
   ├── service
   └── dto
├── domain
   ├── model
   ├── repository
   └── service
├── infrastructure
   ├── persistence
   ├── messaging
   └── configuration
└── interface
    ├── controller
    └── mapper

Dependency rule:

Domain Layer

The domain layer contains the core business logic and domain models.

  • Responsibilities:
    • Encapsulate business rules and behavior.
    • Represent the core concepts of the domain.
  • Key Components:
    • Entities: Objects with a unique identity (e.g., `Todo`).
    • Value Objects: Immutable objects that represent a concept (e.g., `TodoId`).
    • Aggregates: A cluster of domain objects treated as a single unit.
  • Best Practices:
    • Avoid adding persistence-related methods (e.g., `save()` or `delete()`) to domain models.
    • Keep domain models focused on business logic.
  • Example:
public class Todo {
    private TodoId id;
    private String title;
    private String description;
    private boolean completed;

    public Todo(TodoId id, String title, String description) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.completed = false;
    }

    public void markAsCompleted() {
        this.completed = true;
    }

    // Getters and setters
}

Application Layer

The application layer coordinates use cases and orchestrates interactions between the domain and infrastructure layers.

  • Responsibilities:
    • Handle application-specific logic (e.g., workflows, use cases).
    • Delegate persistence to repositories.
  • Example:
public class TodoApplicationService {
    private final TodoRepository todoRepository;

    public TodoApplicationService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    public void createTodo(String title, String description) {
        Todo todo = new Todo(new TodoId(UUID.randomUUID()), title, description);
        todoRepository.save(todo);
    }
}

Infrastructure Layer

The infrastructure layer handles technical concerns like persistence, messaging, and external APIs.

  • Responsibilities:
    • Implement repository interfaces defined in the domain layer.
    • Map between domain models and database entities.
  • Example:
@Repository
public class JpaTodoRepository implements TodoRepository {
    private final SpringDataTodoEntityRepository entityRepository;

    public JpaTodoRepository(SpringDataTodoEntityRepository entityRepository) {
        this.entityRepository = entityRepository;
    }

    @Override
    public void save(Todo todo) {
        TodoEntity entity = mapToEntity(todo);
        entityRepository.save(entity);
    }

    @Override
    public Optional<Todo> findById(TodoId id) {
        return entityRepository.findById(id.getValue())
                               .map(this::mapToDomain);
    }

    private TodoEntity mapToEntity(Todo todo) {
        TodoEntity entity = new TodoEntity();
        entity.setId(todo.getId().getValue());
        entity.setTitle(todo.getTitle());
        entity.setDescription(todo.getDescription());
        entity.setCompleted(todo.isCompleted());
        return entity;
    }

    private Todo mapToDomain(TodoEntity entity) {
        return new Todo(
            new TodoId(entity.getId()),
            entity.getTitle(),
            entity.getDescription(),
            entity.isCompleted()
        );
    }
}

Repository Pattern

The repository pattern abstracts persistence logic and provides access to domain objects.

  • Interface in the Domain Layer:
    • Define the contract for persistence using domain models.
public interface TodoRepository {
    void save(Todo todo);
    Optional<Todo> findById(TodoId id);
    List<Todo> findAll();
}
  • Implementation in the Infrastructure Layer:
    • Implement the repository interface using database-specific entities and persistence mechanisms.

Mapping Between Domain Models and Database Entities

To keep the domain layer independent of persistence, map between domain models and database entities in the infrastructure layer.

  • Domain Model:
public class Todo {
    private TodoId id;
    private String title;
    private String description;
    private boolean completed;

    // Business logic
}
  • Database Entity:
@Entity
@Table(name = "todos")
public class TodoEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String description;
    private boolean completed;

    // Getters and setters
}

Best Practices

  • Use domain models in the repository interface, not database entities.
  • Keep the domain layer free from persistence concerns.
  • Use mappers (manual or libraries like MapStruct) to convert between domain models and database entities.
  • Avoid adding persistence-related methods (e.g., `save()`) to domain models.
  • Use the Unit of Work or event-driven mechanisms for automatic persistence if needed.

Summary

  • The domain layer focuses on business logic and uses repositories to abstract persistence.
  • The infrastructure layer handles persistence and maps between domain models and database entities.
  • Keep the domain layer decoupled from technical concerns to ensure clean, maintainable, and testable code.

FAQ

Difference between repository and persistence

The repository in the domain layer and the persistence layer in the infrastructure serve different purposes and are part of the separation of concerns in DDD.

  • The repository in the domain layer is an abstraction (interface) that defines how the domain interacts with persistence.
  • The persistence layer in the infrastructure is the implementation of that abstraction, handling the actual database operations.

Should I use domain model or database entity in repository?

In the repository interface (defined in the domain layer), you should use domain models because the repository is part of the domain layer and should work with the core business concepts. However, in the repository implementation (in the infrastructure layer), you can map between domain models and database entities to handle persistence.

Should I have a save() method in domain model

No, the domain model (Todo) should not have a save() method. In Domain-Driven Design (DDD), the domain model is responsible for encapsulating business logic and representing the core concepts of the domain. It should not be concerned with persistence or infrastructure details like saving to a database.

Should I handle messages in repository?

Message brokers like Kafka, RabbitMQ, or others are not persistence mechanisms in the traditional sense but are instead used for communication and event-driven architectures. Therefore, creating a repository for a message broker is generally not recommended. Instead, you should handle message brokers in the application layer or infrastructure layer.

  • Do not create a repository for message brokers like Kafka.
  • Use the application layer to handle message publishing/consuming.
  • Abstract the message broker interaction using interfaces and implement them in the infrastructure layer.
  • Leverage domain events to integrate with message brokers in an event-driven architecture.

Whether you should create a repository for Redis depends on how you are using Redis in your application.

  • Use a repository for Redis only if it is a primary data store for domain entities.
  • For caching, use decorators or handle it in the infrastructure layer.
  • For non-domain purposes (e.g., rate limiting, session storage), handle Redis interactions directly in the infrastructure or application layer.
  • Always keep the domain layer free of Redis-specific logic to maintain separation of concerns.

Do I need a service layer?

Whether you need a service layer depends on the complexity of your application and how you structure your code. In Domain-Driven Design (DDD), the service layer (often referred to as the application layer) plays an important role in coordinating use cases and workflows.

You might skip the service layer if:

  • Your application is simple
  • Your domain logic is self-contained