Master DDD: Practical Guide to Domain‑Driven Design Architecture with Code

This article introduces the fundamentals of Domain‑Driven Design (DDD), explains key concepts such as rich domain models, entities, value objects, aggregates, repositories, adapters, and application orchestration, and demonstrates their implementation through comprehensive Java code examples and a complete project structure diagram.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Master DDD: Practical Guide to Domain‑Driven Design Architecture with Code

This article introduces the world of Domain‑Driven Design (DDD) architecture, starting with basic concepts and using vivid examples to illustrate them, and concludes with a diagram of a full DDD project structure.

Architecture design aims to reduce maintenance and iteration costs, which is the purpose of layered architecture.

Traditional MVC architecture is convenient for early development, but as business grows it inevitably degrades.

DDD, or Domain‑Driven Design, is a software design approach that provides techniques such as domains, bounded contexts, entities, value objects, aggregates, factories, and repositories. By following DDD, more time is invested early to plan sustainable, iterative engineering designs.

Rich Domain Model

A rich domain model aggregates both attribute information and behavior logic into a single class. In contrast, an anemic model contains only data attributes, with business logic placed in the service layer.

public class Order {
    private Long id;
    private Long userId;
    private BigDecimal amount;
    // No behavior, all logic in OrderService
}
public class Order {
    private Long id;
    private Long userId;
    private BigDecimal amount;

    // Domain behavior
    public void pay(Payment payment) {
        // payment logic
    }

    public void cancel() {
        // cancel logic
    }
}

Domain Model

A domain model captures core business concepts, combining data and behavior to reflect real‑world scenarios and rules. In DDD, the recommended implementation is a rich domain model, meaning the model contains its own business logic.

Domain Model = Rich Model + Aggregates, Entities, Value Objects, Factories, etc.

Entity

An entity has a unique identifier that remains constant even if its attributes change. For example, User and Order are entities; the OrderId never changes even if the order details are updated.

public class User {
    private Long id; // unique identifier
    private String name;
    private int age;
    // behavior methods
}

Value Object

A value object has no unique identifier; equality is based on attribute values. Value objects are immutable. For example, an Address is a value object—two addresses are equal only if all fields match.

// No id, immutable after creation
public class Address {
    private final String province;
    private final String city;
    private final String street;

    public Address(String province, String city, String street) {
        this.province = province;
        this.city = city;
        this.street = street;
    }
    // No setters
}

Aggregate

An aggregate groups closely related entities and value objects into a bounded business module. The aggregate root is the only entry point for accessing or modifying the aggregate's internal objects.

public class Order { // Aggregate root
    private Long orderId; // unique identifier
    private List<OrderItem> items; // child entities
    private Address address; // value object

    public void addItem(OrderItem item) { /* ... */ }
    public void changeAddress(Address address) { /* ... */ }
}

The aggregate root’s unique identifier allows external systems, repositories, or caches to reference the entire aggregate via a single ID.

Repository

A repository bridges the domain model and persistence technology, providing a domain‑oriented interface for finding and saving domain objects without exposing database details.

public interface OrderRepository {
    Order findById(Long id); // find Order by id
    void save(Order order);   // save Order
    void delete(Long id);    // delete Order by id
}

Implementation resides in the infrastructure layer, handling caching, database access, and remote storage.

public class OrderRepositoryImpl implements OrderRepository {
    private final RedisTemplate redisTemplate;
    private final OrderDao orderDao; // MyBatis
    private final FileStorageService fileStorageService;
    private final ArchiveRemoteService archiveRemoteService;

    @Override
    public Order findById(Long id) {
        // 1. Check cache
        Order order = redisTemplate.get("order:" + id);
        if (order != null) return order;
        // 2. Query DB
        order = orderDao.selectById(id);
        if (order != null) {
            // 3. Populate cache
            redisTemplate.set("order:" + id, order, 1, TimeUnit.HOURS);
        }
        return order;
    }

    @Override
    public void save(Order order) {
        // 1. Write DB
        orderDao.save(order);
        // 2. Sync cache
        redisTemplate.set("order:" + order.getId(), order, 1, TimeUnit.HOURS);
    }

    @Override
    public void delete(Long id) {
        // 1. Delete DB
        orderDao.delete(id);
        // 2. Delete cache
        redisTemplate.delete("order:" + id);
    }

    public void archive(Order order) {
        // Write to local archive file
        fileStorageService.save(order);
        // Upload to remote archive center
        archiveRemoteService.upload(order);
    }
}

Adapter

An adapter acts as a bridge to external systems, allowing the domain layer to define interfaces without caring about implementation details.

// domain/service/SmsSender.java
public interface SmsSender {
    void sendCode(String mobile, String code);
}
// infrastructure/adapter/AliyunSmsSender.java
public class AliyunSmsSender implements SmsSender {
    private final AliyunSmsClient aliyunSmsClient;
    public AliyunSmsSender(AliyunSmsClient aliyunSmsClient) {
        this.aliyunSmsClient = aliyunSmsClient;
    }
    @Override
    public void sendCode(String mobile, String code) {
        // encapsulate third‑party API details
        aliyunSmsClient.send(mobile, code);
    }
}

If a different provider (e.g., Tencent Cloud) is needed, a new implementation can be added without changing domain code.

Application Layer (Domain Orchestration)

The application layer composes multiple domain services to implement complex business processes, acting like glue that assembles independent bounded contexts.

public class OpenAccountOrchestrationService {
    private final UserService userService;
    private final MarketingService marketingService;
    private final NotifyService notifyService;

    public void openAccountAndGiftCoupon(String userId) {
        userService.openAccount(userId);
        marketingService.giftCoupon(userId);
        notifyService.sendAccountOpenMessage(userId);
    }
}

Trigger Layer

The trigger layer (e.g., controllers, message listeners, schedulers) receives external inputs and invokes the application layer, keeping business logic out of the entry points.

@RestController
@RequestMapping("/user")
public class UserController {
    private final UserOrchestrationService userOrchestrationService;
    @PostMapping("/register")
    public ResultVO register(@RequestBody RegisterRequest req) {
        userOrchestrationService.registerAndGiftCoupon(req.getMobile());
        return ResultVO.success();
    }
}
@Component
public class CouponExpireScheduler {
    private final CouponOrchestrationService couponOrchestrationService;
    @Scheduled(cron = "0 0 2 * * ?") // every day at 2 AM
    public void expireCoupons() {
        couponOrchestrationService.expireOverdueCoupons();
    }
}

For small projects, direct calls from the trigger layer to domain services may be acceptable; for larger, more complex systems, adding an application orchestration layer improves modularity and flexibility.

Example Project Structure

src/
├── domain/                     # Core business objects and rules
│   ├── user/
│   │   ├── User.java
│   │   ├── UserRepository.java
│   │   ├── UserService.java
│   ├── coupon/
│   │   ├── Coupon.java
│   │   ├── CouponRepository.java
│   │   ├── CouponService.java
│   ├── notify/
│   │   ├── SmsSender.java   # Adapter interface
│   │   ├── NotifyService.java
│
├── application/               # Application/orchestration layer
│   ├── OpenAccountOrchestrator.java
│   ├── CouponExpireOrchestrator.java
│
├── trigger/                    # Entry points (HTTP, MQ, Scheduler)
│   ├── OpenAccountController.java
│   ├── OpenAccountListener.java
│   ├── CouponExpireScheduler.java
│
├── infrastructure/             # Technical implementations
│   ├── repository/
│   │   ├── UserRepositoryImpl.java
│   │   ├── CouponRepositoryImpl.java
│   ├── adapter/
│   │   ├── AliyunSmsSender.java
│   │   ├── RedisCacheAdapter.java
│   ├── config/
│   │   ├── DataSourceConfig.java
│
├── common/                     # Utilities, VO, exceptions
│   ├── ResultVO.java
│   ├── BizException.java
│
└── MainApplication.java        # Application entry point
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.

architectureDomain-Driven DesignDDDRepositoryrich domain model
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

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.