DDD, SAGA Pattern, and Outbox Pattern with Real Code – Part 1

By | January 27, 2025

Intro

DDD (Domain-Driven Design) is a fairly popular approach that, while facing some criticism, has benefits that are evident for complex projects.

Reading theory is one thing, but diving into code implementation is entirely different – it provides hands-on experience and a deeper understanding. Practice is practice.

We’ll start with the theory of DDD and refer back to it as we analyze the code. Also, if there’s something unclear, please refer to these books for details:

I recommend “Implementing Domain-Driven Design” book as it contains a lot of details that are not available in this cycle of articles (we are mostly focused on the concrete source code). We are going to better understand DDD by analyzing source code – https://github.com/TorinKS/food-ordering-system/tree/master from an excellent course—https://www.udemy.com/course/microservices-clean-architecture-ddd-saga-outbox-kafka-kubernetes/ created by Ali Gelenler, so let’s start.

There are two sides of DDD (we skip ubiquitous language):

  • strategical
  • tactical

Strategic side is all about aligning business model with the software model, it includes:

  • Bounded Context. Defines the boundaries of a specific model in the domain.
  • Ubiquitous Language. A shared language used by developers and domain experts to minimize miscommunication.
  • Context Mapping. Highlights how different bounded contexts relate to each other.
  • Core Domain. Identifies the most critical and valuable part of the domain.

Tactical design deals with the implementation details within a bounded context. It provides patterns and building blocks to implement the domain model effectively. Key concepts include

  • Entities. Objects with a unique identity that persists over time.
  • Value Objects. Immutable objects that represent descriptive aspects of the domain with no identity.
  • Aggregates. A group of entities and value objects treated as a single unit for consistency.
  • Repositories. Facilitate retrieval and persistence of aggregates.
  • Domain Services. Encapsulate domain logic that doesn’t naturally fit into an entity or value object.

Tactical design is about creating clean, maintainable, and meaningful implementations of domain logic.

These two sides work together to ensure that the software is not only technically sound but also deeply connected to the business needs. Strategic design provides the framework for organizing the problem space, while tactical design helps to solve specific problems within that framework.

The code we are learning is an online food ordering system for restaurants. There are on domain and two subdomains:

  • food ordering application (domain)
  • suddomains (order processing, payment processing)

DDD entities

Entities are unique objects with unique identifiers that do not change during their lifespan. They are key building blocks of the domain model and represent business concepts that are modeled in our code. The properties and methods of entities can change due to shifts in business requirements.

The examples of entities are :

Value objects

Value objects – simple blocks used in DDD that have these characteristics:

  • they are compared based on their attributes. They don’t use unique ID properties.
  • they should represent meaningful concepts from the domain, encapsulating business logic related to those concepts.
  • they should ensure they are always in a valid state when created.

The benefits of using Values objects are:

  • they make the domain model richer and more understandable by representing domain concepts explicitly.
  • they reduce bugs related to unintended side effects.
  • they are simple to test.
  • they encapsulate domain logic specific to their concept, improving cohesion and readability.

These classes are Value Objects

Let’s look at one of them, StreetAddress class

package com.food.ordering.system.order.service.domain.valueobject;

import java.util.Objects;
import java.util.UUID;

public class StreetAddress {
    private final UUID id;
    private final String street;
    private final String postalCode;
    private final String city;

    public StreetAddress(UUID id, String street, String postalCode, String city) {
        this.id = id;
        this.street = street;
        this.postalCode = postalCode;
        this.city = city;
    }

    public UUID getId() {
        return id;
    }

    public String getStreet() {
        return street;
    }

    public String getPostalCode() {
        return postalCode;
    }

    public String getCity() {
        return city;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StreetAddress that = (StreetAddress) o;
        return street.equals(that.street) && postalCode.equals(that.postalCode) && city.equals(that.city);
    }

    @Override
    public int hashCode() {
        return Objects.hash(street, postalCode, city);
    }
}

As we can see, this class meetsthe criteria mentioned previously:

  • there is no unique id
  • it encapsulates domain logic
  • it is simple to test , etc

Of course, we can use something like value objects in our code even if we don’t use DDD, and just use layered architecture with presentation / service / data layer, and just want to make our code more testable and flexible for changing.

common-domain module

Also, part of the general functionality has moved to common-domain module of the project:

  • public abstract class AggregateRoot
  • public abstract class BaseEntity
  • public interface DomainEventPublisher
  • public class DomainException
  • public abstract class BaseId

The root of the hierarchy is the public abstract class BaseEntity<ID>, which is extended by the public abstract class AggregateRoot<ID>. Thus, AggregateRoot is just one of the entity types. Generic variables are used here to parameterize each class with its specific identifier, allowing us to use UUID for some classes and specific ID classes for others, such as:

  • CustomerId
  • OrderId
  • ProductId

UML class diagram below for Restaurant AggregateRoot from com.food.ordering.system.order.service.domain.entity package depicts relationships between these classes.

BaseEntity class provides functionality of:

  • getId
  • setId
  • equals based on equality of ID generic variable

This class is important for entities as each entity has to have unique identifier. AggregateRoot class is explicitly added to the code (as mark class) to distinct entities such as CreditEntry, OrderItem, etc from Aggregate Roots.

Finally, we have two domain entities related classes reused in other packages:

  • abstract class BaseEntity<ID>
  • abstract class AggregateRoot<ID> extends BaseEntity<ID>

Value objects have its specific functionality so all abstract classes / enums can be found in com.food.ordering.system.domain.valueobject package, it includes:

  • abstract class BaseId<T> which provides base contract for specific “ID classes”: CustomerId, OrderId, ProductId, RestaurantId, etc
  • some enumerations: OrderApprovalStatus, PaymentOrderStatus, RestaurantOrderStatus
  • Money class that encapsulates money specific code, like rounding needed in financial calculations (Banker’s Rounding) that is used in different services (OrderService and PaymentService)
  • OrderId as it is reused in different packages not only order-service

Moving from general functionality to concrete implementation, take a look at the com.food.ordering.system.order.service.domain package. It contains its specific model and additional code and consists of two additional modules:

  • order-application-service
  • order-domain-core

order-application-service (https://github.com/TorinKS/food-ordering-system/tree/master/order-service/order-domain/order-application-service) has code responsible for implementation of:

  • DTOs
  • intput and output ports (see later about Hexagonal Architecture) interfaces (the concrete implementations are of course outside of the domain logic)
  • mappers
  • outbox pattern
  • SAGA pattern
  • helpers

order-domain-core (https://github.com/TorinKS/food-ordering-system/tree/master/order-service/order-domain/order-domain-core) contains our model:

  • Aggregate roots
  • entities (Product, OrderItem)
  • events implementations (OrderCancelledEvent, OrderCreatedEvent, OrderEvent, OrderPaidEvent)

Aggregates and Aggregate Roots

Aggregates – are group of logically realetd entities. You can read more about Aggregates in Recommended literature section, just take a note:

A properly designed Aggregate is one that can be modified in any way required by the business with its invariants completely consistent within a single transaction. And a properly designed Bounded Context modifies only one Aggregate instance per transaction in all cases. What is more, we cannot correctly reason on Aggregate design without applying transactional analysis. [2]

In our project, we have “Order processing” aggregate with entities:

  • Order
  • Order Item
  • Product

Each aggregate is owned by AggregateRoot. Aggregate root examples are:

We use Builder pattern for Order (AggregateRoot) with final properties such as:

  • CustomerId customerId;
  • RestaurantId restaurantId;
  • StreetAddress deliveryAddress;
  • Money price;

to make objects immutable and keep a flexible way to create instances without creating multiple constructors.

The Aggregate Root is the interface for communication with the Aggregate from the outside. Business related operations like queries / modifications of Aggregate have to use only Aggregate Root. Also, Aggregate Root is responsible for the consistency of the aggregate’s state enforcing invariants across its entities and value objects. We also can’t access directly entities and value objects without interacting with aggregate roots.

If we check source code of Order AggregateRoot https://github.com/TorinKS/food-ordering-system/blob/2b41ee558e281979abc8fe3790ba16435e599017/order-service/order-domain/order-domain-core/src/main/java/com/food/ordering/system/order/service/domain/entity/Order.java#L13 we’ll see that it is responsible for:

  • initializeOrder
  • validateOrder
  • pay
  • approve
  • initCancel
  • cancel
  • validateInitialOrder
  • validateTotalPrice
  • validateItemsPrice
  • validateItemPrice

and this is its external interface to outside world. The OrderStatus for example can be changed only using methods available in AggregateRoot.

Aggregate Roots such as Payment are created in:

  • PaymentDataAccessMapper.paymentEntityToPayment(mapper in payment-dataaccess )
  • PaymentDataMapper.paymentRequestModelToPayment(mapper in payment-application-service)

So for examle, when “receive” method of PaymentRequestKafkaListener is called (when new event in payment-request kafka topic arrives) it calls completePayment from PaymentRequestMessageListenerImpl which calls persistPayment that uses PaymentDataMapper.paymentRequestModelToPayment() that returns Payment Aggregate Root.

Domain Service

Next DDD related object is Domain Service, see example https://github.com/TorinKS/food-ordering-system/blob/master/order-service/order-domain/order-domain-core/src/main/java/com/food/ordering/system/order/service/domain/OrderDomainService.java. The key characteristics of Domain Service:

  • they belong to the domain model and don’t contain any dependencies on UI, data later, etc
  • methods of Domain Service use (should use) terminology of the domain
  • they avoid transaction and state management

As you can see, based on an example from this code

public interface OrderDomainService {

    OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant);

    OrderPaidEvent payOrder(Order order);

    void approveOrder(Order order);

    OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages);

    void cancelOrder(Order order, List<String> failureMessages);
}

the interface belongs to order domain core which is the part of the order domain and the methods of interface use terminology of the business model.

OrderDomainService is annotated as bean

@Configuration
public class BeenConfiguration {
    @Bean
    public OrderDomainService orderDomainService() {
        return new OrderDomainServiceImpl();
    }
}

and injected by SpringBoot in OrderCreateHelper

public class OrderCreateHelper {
    private final OrderDomainService orderDomainService;
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final RestaurantRepository restaurantRepository;
    private final OrderDataMapper orderDataMapper;

One of the Domain Service’s responsibility is to coordinate the logic across multiple aggregates as shown below

Domain services are not accessible from the outside as they belong to the Domain Core. Therefore, Application Services play this role instead. Application Services are also allowed to communicate to other Domain Services.

Application Service

Application Services are used for isolation of core domain logic from the outside. They don’t contain any business related logic. Their responsibilities are:

  • transactions orchestration
  • persistance changes in the database
  • security

They expose domain API to the outside and hide details of the domain logic. You can notice that when such class as OrderDomainServiceImpl is used for Domain Service imlpementation, OrderCreateHelper – https://github.com/TorinKS/food-ordering-system/blob/master/order-service/order-domain/order-application-service/src/main/java/com/food/ordering/system/order/service/domain/OrderCreateHelper.java is the application service. It persists data in database

public OrderCreatedEvent persistOrder(CreateOrderCommand createOrderCommand) {

gets the required information, performs other required actions.

OrderApplicationServiceImpl is used in REST API Controller – https://github.com/TorinKS/food-ordering-system/blob/2b41ee558e281979abc8fe3790ba16435e599017/order-service/order-application/src/main/java/com/food/ordering/system/order/service/application/rest/OrderController.java#L29

They are typically very “thin” and are used mostly for coordination, keeping other logic to the other layers.

Domain Events

Another important aspect of DDD are Domain Events. They are used for purposes such as supporting asynchronous communication and distributed transactions using the SAGA pattern (as we will discuss later). Each time a new domain event occurs (e.g., when a Kafka listener receives a new event), the domain event listener is triggered and performs its logic, as shown in the diagram below.

That’s enough for a brief introduction to DDD. Now, let’s explore the architecture patterns used in the code.

Clean architecture and Hexagonal architecture

Clean architecture is software design approach that allows to create maintenable, flexible for changing, testable code by isolation domain logic from the outside dependencies. It allows you to implement architectures that are adaptable for further changing. I recommend “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” (Robert C. Martin Series) book for more details.

Hexagonal Architecture (Ports and Adapters Architecture) is a software architecture pattern whose principles are closely aligned with those of Clean Architecture. We can say that Hexagonal Architecture follows the principles of Clean Architecture. Both emphasize separation of concerns, dependency inversion, and independence from frameworks, UI, databases, and external systems

The diagram below depicts main components of Hexagonal architecture

You can familiarize yourself with Hexagonal Architecture by reading these sources that I personally found useful

Shortly, about details of Hexagonal architecture. There are input and output ports.

Primary adapters are the implementation of input ports, the examples of the code:

  • PaymentRequestMessageListenerImpl class

Output ports are adapter to the external systems domain model that interacts with external world, source code examples are:

  • PaymentEventKafkaPublisher
  • CreditEntryRepositoryImpl
  • CreditHistoryRepositoryImpl
  • OrderOutboxRepositoryImpl
  • PaymentRepositoryImpl

The code follows DIP (Dependency Inverstion Principles) when the domain logic has interfaces which are implemented by the external (input and output ports) so it doesn’t depend on implementation details.

Data Access Mappers

They play a significant role in converting JPA Entities (POJO) objects to Domain objects. See DDD domain objects, entities and anemic model for mode details.

[1] Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans

[2] Implementing Domain-Driven Design by Vaughn Vernon. This book is focused on how to implement DDD.

[3] Domain-Driven Design Quickly – https://www.infoq.com/minibooks/domain-driven-design-quickly/

[4] Clean Architecture: A Craftsman’s Guide to Software Structure and Design” (Robert C. Martin Series) book

Leave a Reply