Understanding Domain-Driven Design (DDD): Concepts, Code Examples, and Practical Scenarios
This article explains Domain-Driven Design (DDD), contrasting it with traditional layered development through clear examples, outlines key DDD building blocks such as aggregate roots, domain services, and events, and discusses when DDD is appropriate for complex, frequently changing business domains.
Introduction
In everyday development we often hear about DDD, but many find the concept vague and hard to grasp. This article aims to provide a clear and simple understanding of Domain-Driven Design.
1. What is DDD
Domain-Driven Design (DDD) is a software development approach that focuses on modeling complex systems by deeply aligning code structure with the business domain. In a single sentence: use code to reproduce the essence of the business rather than merely implementing features.
For traditional development, business logic is scattered across controllers, services, and utilities. In DDD, developers collaborate with business stakeholders to draw domain models, and the code becomes a mirror of the business.
2. Traditional Development Example – User Registration
Consider a simple user‑registration scenario with rules such as unique usernames, password complexity, and logging.
@Controller
public class UserController {
public void register(String username, String password) {
// validate password
// check username
// save to database
// log action
// all logic mixed together
}
}Even after adding layers (controller, service, DAO) the code still mixes responsibilities:
// Service layer – only flow control, business rules are scattered
public class UserService {
public void register(User user) {
// rule 1 in utility class
ValidationUtil.checkPassword(user.getPassword());
// rule 2 via annotation
if (userRepository.exists(user)) { ... }
// directly pass data to DAO
userDao.save(user);
}
}Many developers mistakenly think that simply adding layers makes the code "elegant" and consider it DDD, which is not true.
3. Is Layering Enough for DDD?
No. Although the code is layered, the User object is merely a data carrier (anemic model) and business logic resides outside the domain object.
In DDD the same registration example would look like this, using a rich (charged) model:
// Domain entity – business logic inside
public class User {
public User(String username, String password) {
if (!isValidPassword(password)) {
throw new InvalidPasswordException();
}
this.username = username;
this.password = encrypt(password);
}
private boolean isValidPassword(String password) { /* ... */ }
}Here the password rule is encapsulated within the entity, and the object is no longer just a "data bag".
4. Key DDD Designs
Beyond layering, DDD introduces several patterns that deepen business expression:
Aggregate Root
Domain Service vs. Application Service
Domain Events
4.1 Aggregate Root
Example: a User aggregates multiple Address objects. The User entity controls addition of addresses.
public class User {
private List
addresses;
public void addAddress(Address address) {
if (addresses.size() >= 5) {
throw new AddressLimitExceededException();
}
addresses.add(address);
}
}4.2 Domain Service vs. Application Service
Domain services contain core business logic that spans multiple entities, while application services orchestrate workflows without embedding business rules.
// Domain service – core logic
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.debit(amount);
to.credit(amount);
}
}
// Application service – orchestration
public class BankingAppService {
public void executeTransfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
transferService.transfer(from, to, new Money(amount));
messageQueue.send(new TransferEvent(...));
}
}4.3 Domain Events
Events explicitly express business changes, e.g., a UserRegisteredEvent after successful registration.
public class User {
public void register() {
// ... registration logic
this.events.add(new UserRegisteredEvent(this.id));
}
}5. Traditional Development vs. DDD
Dimension
Traditional Development
DDD
Business Logic Ownership
Scattered across Service, Util, Controller
Encapsulated in domain entities/services
Model Role
Data carrier (anemic)
Behavior‑rich business model (charged)
Technical Influence
Database‑driven design
Business‑driven design
6. E‑commerce Order DDD Example
Requirement: create an order that validates stock, applies coupons, calculates the payable amount, and generates the order.
Traditional (anemic) implementation mixes validation, calculation, and persistence in a service:
public class OrderService {
@Autowired private InventoryDAO inventoryDAO;
@Autowired private CouponDAO couponDAO;
public Order createOrder(Long userId, List
items, Long couponId) {
// stock validation scattered in loop
// total price calculation
// coupon application via utility
// persist order
return order;
}
}Problems: business rules are dispersed, Order is a plain data object, and changes require digging through multiple layers.
DDD (rich) implementation moves logic into the aggregate root:
public class Order {
private List
items;
private Coupon coupon;
private Money totalAmount;
public Order(User user, List
items, Coupon coupon) {
items.forEach(item -> item.checkStock());
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
if (coupon != null) {
validateCoupon(coupon, user);
this.totalAmount = coupon.applyDiscount(this.totalAmount);
}
}
private void validateCoupon(Coupon coupon, User user) {
if (!coupon.isValid() || !coupon.isApplicable(user)) {
throw new InvalidCouponException();
}
}
}
public class OrderService {
public Order createOrder(User user, List
items, Coupon coupon) {
Order order = new Order(user, convertItems(items), coupon);
orderRepository.save(order);
domainEventPublisher.publish(new OrderCreatedEvent(order));
return order;
}
}Advantages: stock check lives in OrderItem , coupon rules in Order , and any business change only touches the domain layer.
7. When to Use DDD?
✅ Complex business domains (e‑commerce, finance, ERP)
✅ Frequently changing requirements (most internet services)
❌ Simple CRUD applications (admin panels, reporting)
In essence, DDD shines when modifying business rules only requires changes in the domain layer, leaving controllers, DAOs, and infrastructure untouched.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.