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.
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 pointSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.
