Fundamentals 12 min read

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.

IT Services Circle
IT Services Circle
IT Services Circle
Understanding Domain-Driven Design (DDD): Concepts, Code Examples, and Practical Scenarios

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.

backenddesign patternssoftware architectureDomain-Driven DesignDDD
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login 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.