Fundamentals 31 min read

How to Apply Domain-Driven Design: Strategies, Architecture, and Code Samples

This article explains the origins of Domain‑Driven Design, describes strategic concepts such as domain, bounded context and context mapping, explores tactical building blocks like entities, value objects, services, aggregates and factories, and demonstrates practical architecture choices and Java code examples for a payment system.

SQB Blog
SQB Blog
SQB Blog
How to Apply Domain-Driven Design: Strategies, Architecture, and Code Samples

Background

Eric Evans first introduced the concept of Domain‑Driven Design (DDD) in his 2003 book, describing it as a method to reduce software complexity. Vaughn Vernon expanded the ideas in his 2013 book Implementing Domain‑Driven Design .

Introduction

Although Java is an object‑oriented language, traditional three‑layer architectures often use procedural coding where data models are mere carriers and business logic lives in the service layer. This leads to "anemic domain objects" as Vernon calls them.

Procedural approaches can speed up early development but create technical debt. As business logic grows, code becomes bloated, hard to read, and slows delivery and stability.

To address this, we adopted DDD for both new system construction and legacy refactoring, using strategic and tactical design phases.

Strategic Design

Domain

Domain is business, the things an organization does and everything it contains.

Vernon distinguishes sub‑domains: core, supporting, and generic. For an e‑commerce system, sub‑domains include Order, Invoice, etc., aligning naturally with microservice boundaries.

Bounded Context

Bounded context is an explicit boundary within which a domain model is defined. The same name may have different meanings in different contexts.
com.mycompany.xxx

In practice bounded contexts are usually defined under department packages: com.CompanyName.DepartmentName.BoundedContext Typical secondary package names:

com.mycompany.team.xxx.presentation
com.mycompany.team.xxx.application
com.mycompany.team.xxx.domain.model
com.mycompany.team.xxx.infrastructure

Context Mapping Diagram

When adopting DDD, first draw a context‑mapping diagram that shows current bounded contexts and their integration relationships.

Abbreviations used in the diagram:

U – Upstream

D – Downstream

ACL – Anticorruption Layer

OHS – Open Host Service

PL – Published Language

Architecture

DDD does not mandate a specific architecture. Because the core domain lives inside a bounded context, many architectural styles can be used.

Layered Architecture

Traditional layered architecture places the core domain in the domain layer, with presentation above and infrastructure below. Strict layering allows coupling only to the immediate lower layer; relaxed layering permits any upper layer to depend on any lower layer.

Application services coordinate domain objects and should be lightweight, handling transactions, security checks, or event publishing.

Placing repository interfaces in the domain layer while implementations reside in the infrastructure layer violates the Dependency Inversion Principle and can cause circular dependencies.

Hexagonal Architecture

Clients interact with the system through equal ports; adding a new client only requires a new adapter that translates client input into the system’s API.

CQRS

Complex queries (e.g., multi‑condition pagination) are handled by separating command and query models. Commands modify aggregates; queries read from a separate read model updated asynchronously via domain events.

Tactical Design

Common domain‑model building blocks:

Entity

An entity has a unique identity that persists over time even as its attributes change.

Example: a person’s ID card number remains constant regardless of name or address changes.

Value Object

A value object describes an aspect of the domain without its own identity.

Characteristics:

It measures or describes something in the domain.

It can be immutable.

It combines related attributes into a conceptual whole.

When its state changes, it is replaced by a new instance.

It can be compared for equality.

It has no side effects.

Money is a typical value object. A complete representation includes amount, unit, and currency:

{
  "amount":"123",
  "unit":"Yuan/Fen",
  "currency":"CNY/USD"
}
public class Money {
  private String amount;
  private MoneyUnit moneyUnit;
  private Currency currency;
  public long getFen() {}
  public String getYuan() {}
  public long getTargetCurrencyFen(BigDecimal exchangeRate) {}
}

Domain Service

Domain services encapsulate operations that do not naturally belong to an entity or value object.

They should be stateless, defined by the domain model, and focus on behavior rather than data.

Domain Event

Domain events capture significant occurrences within the domain.

Events may be published locally or to remote bounded contexts, enabling asynchronous processing.

Aggregate

An aggregate is a cluster of related objects treated as a single unit for data changes.

Key rules:

Model true invariants inside the consistency boundary; only one aggregate is modified per transaction.

Design small aggregates with a root entity that contains the minimal necessary attributes or value objects.

Reference other aggregates by their root identifiers rather than by holding direct object references.

Use eventual consistency outside the boundary, often via domain events.

Exceptions may be made for UI convenience, lack of technical mechanisms, global transactions, or query performance concerns.

public class OrderAggrRoot {
  private String id;
  private Seller seller; // value object snapshot
  private Buyer buyer;   // value object snapshot
  private TradeInfo tradeInfo; // value object snapshot
  private Money money; // value object
  private OrderStatus status; // value object
  private List<TransactionEntity> transactionList; // entities
  public boolean isRefundable() {}
  public boolean isCanBeCancelled() {}
  public void processPayResult(PayResult result) {}
  public void processRefundResult(RefundResult result) {}
}

Factory

Factories encapsulate complex creation logic for aggregates.
public class CarAggrRoot {
  CarAggrRoot() {}
  private String id;
  private String color;
  private Engine engine;
  private List<Wheel> wheels;
  private Chassis chassis;
}
public class CarFactory {
  public static CarAggrRoot genCar(String id, String color, Engine engine,
                                   List<Wheel> wheels, Chassis chassis) {
    // validation logic …
    return new CarAggrRoot(id, color, engine, wheels, chassis);
  }
}

Repository

Repositories provide safe storage and retrieval of aggregates, abstracting persistence details.

They hide serialization, deserialization, and data‑source specifics from the domain.

Practice

Payment Core Domain

We divided the payment domain into several sub‑domains:

Core sub‑domain: order and transaction (the payment core).

Channel support sub‑domain: interaction with third‑party payment channels.

Promotion support sub‑domain: discounts and marketing activities.

Risk‑control support sub‑domain: fraud detection and security checks.

Merchant common sub‑domain: merchant data used by multiple sub‑domains.

Context Mapping Diagram

The core order context accesses merchant, risk, channel, and promotion sub‑domains through an anticorruption layer. Offline payment and smart store are external domains accessed via open host services.

Code Implementation

// Order aggregate (domain layer)
public class OrderAggrRoot {
  private String id; // order id
  private Seller seller; // value object snapshot
  private Buyer buyer;   // value object snapshot
  private TradeInfo tradeInfo; // value object snapshot
  private Money money; // value object
  private OrderStatus status; // value object
  private List<TransactionEntity> transactionList; // entities
  public boolean isRefundable() {}
  public boolean isCanBeCancelled() {}
  public void processPayResult(PayResult result) {}
  public void processRefundResult(RefundResult result) {}
}
// Pay result model (infrastructure layer, anticorruption)
public class PayResult {
  // fields …
  public boolean isSuccess() {}
  public boolean isFailure() {}
  public boolean isUnknown() {}
}
// Trade application service (application layer)
public class TradeApplicationServiceImpl {
  @Resource private OrderRepository orderRepository;
  @Resource private OrderDomainService orderDomainService;
  @Resource private TradeConfigClient tradeConfigClient;
  @Resource private ChannelRoutingClient routingClient;

  @Transactional
  public PayResponse pay(PayContext context) {
    context.checkParams();
    TradeConfig tradeConfig = tradeConfigClient.queryTradeConfig(context);
    tradeConfig.check();
    OrderAggrRoot orderAggrRoot = orderDomainService.createOrder(context);
    orderRepository.add(orderAggrRoot);
    PayResult result = routingClient.pay(context.genPayRoutingRequest);
    orderAggrRoot.processPayResult(result);
    orderRepository.update(orderAggrRoot);
    return genResponse();
  }
}
// Order domain service (domain layer)
public class OrderDomainServiceImpl {
  @Resource private PreferentialServiceClient preferentialServiceClient;
  public OrderAggrRoot createOrder(BasePayContext context) {
    DiscountInfo discountInfo = preferentialServiceClient.queryDiscountInfo();
    OrderAggrRoot orderAggrRoot = OrderFactory.createOrder(context, discountInfo);
    return orderAggrRoot;
  }
}
// Anticorruption layer for external discount service
public class PreferentialServiceClient {
  @Resource private PreferentialService preferentialService;
  public DiscountInfo queryDiscountInfo(DiscountQueryRequest request) {
    DiscountResult result = preferentialService.queryDiscount(request.genRequest());
    return genDiscountInfo(result);
  }
  private DiscountInfo genDiscountInfo(DiscountResult result) {}
}

External services should be accessed through an anticorruption layer to translate external models into local ones, preserving core domain stability.

Conclusion

After studying DDD for several years, the author began applying it in 2021 to refactor a critical system. Despite challenges such as entity design and aggregate loading, the resulting system is stable, readable, and easy to extend, with clear strategic guidance for future domain divisions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaarchitecturemicroservicesDomain-Driven DesignStrategic DesignTactical Design
SQB Blog
Written by

SQB Blog

Thank you all.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.