Domain-Driven Design in Java: Not Just Theory, but Live Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why DDD Is Not a Trendy Term, But a Project-Saving Tool



Every Java developer has encountered this situation at least once: the code works, business logic is scattered across services, and after six months, only the author (and even they with difficulty) can understand the project. Sound familiar? This is the classic anemic domain model — where entities are just POJOs with getters and setters, and all the logic lives in "fat" services.



This is where Domain-Driven Design (DDD) enters the stage — an approach that places business logic, not technical details, at the center of development. And no, this is not another "theoretical" methodology. DDD in Java projects provides concrete patterns that turn chaos into a clean, maintainable architecture. In this article, we will break down DDD in practice: with code, examples, and no fluff.



Strategic vs Tactical DDD: Which One to Choose for a Java Project?



DDD is divided into two levels: strategic and tactical. The first helps break a large system into Bounded Contexts and establish communication between them. The second provides concrete building blocks for code: Entity, Value Object, Aggregate, Repository, Domain Event, etc.



For a Java developer, tactical DDD is where you should start. It provides tools you can apply today. But without a strategic understanding of contexts, you risk creating a "universal" model that doesn't fit anyone. So let's start small: we'll break down the key patterns using a simple but real domain — an order management system.



DDD Building Blocks in Java: From Entity to Aggregate



1. Entity — Each Has Its Own Identity



In DDD, an Entity is an object that has a unique identifier and a continuous life line. You can change all its fields, but it remains the same entity. In Java, this is not just a POJO — it's an object with behavior.



Example: an order in an online store. Even if the order status changes, it is still the same order with the same ID.



public class Order {    private final OrderId id; // Value Object, not a primitive    private OrderStatus status;    private List<OrderLineItem> items;    private Money totalAmount;

public Order(OrderId id) { this.id = id; this.status = OrderStatus.PENDING; this.items = new ArrayList<>(); this.totalAmount = Money.ZERO; }

public void addItem(Product product, int quantity) { // Business rule: cannot add an item to a confirmed order if (this.status != OrderStatus.PENDING) { throw new IllegalStateException("Cannot add items to a non-pending order"); } OrderLineItem item = new OrderLineItem(product, quantity); items.add(item); recalculateTotal(); }

public void confirm() { if (items.isEmpty()) { throw new IllegalStateException("Cannot confirm an empty order"); } this.status = OrderStatus.CONFIRMED; // Here you can fire a domain event // registerEvent(new OrderConfirmedEvent(this.id)); }

private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderLineItem::getSubtotal) .reduce(Money.ZERO, Money::add); }

// Getters — read-only, no setters! public OrderId getId() { return id; } public OrderStatus getStatus() { return status; } public List<OrderLineItem> getItems() { return Collections.unmodifiableList(items); } public Money getTotalAmount() { return totalAmount; }}


Notice: OrderId is not a Long, but a Value Object. Why? Because a primitive type carries no semantics. OrderId can contain a prefix, creation date, or pr

Blogs

Book Recommendations