Master Domain-Driven Design: From Theory to a Complete Java DDD Demo
This article explains the fundamentals of Domain‑Driven Design, compares it with traditional MVC, details layered architecture, data transformation objects, core concepts like aggregates and domain events, and provides a full Java SpringBoot DDD demo with code, project structure, database schema, and deployment steps.
1. Introduction to DDD
1.1 Why Use DDD?
Object‑oriented design with data‑behavior binding, eliminating anemic models.
Reduce complexity through divide‑and‑conquer.
Prioritize domain model over splitting data and behavior.
Accurately convey business rules; business first.
Code is design.
Boundaries simplify complex domains and enable unified architectural evolution.
Share domain knowledge to improve collaboration.
Increase maintainability, readability, and software lifespan.
Foundation for middle‑platform architecture.
1.2 Role of DDD
In MVC three‑tier architecture, development usually starts with table design, then DAO, service, and controller, converting requirements into data structures. This often leads to business logic being scattered across layers, making maintenance hard.
For an e‑commerce order scenario, MVC would design separate tables for orders, payments, products, etc., and continuously modify them as features evolve, resulting in tangled code.
DDD first defines business boundaries; the order becomes the aggregate root, and related concepts (payment, product, address) are modeled around it, keeping the domain model clean.
DDD overall benefits:
Eliminate information asymmetry.
Reverse the bottom‑up MVC design to a top‑down, business‑driven approach.
Split large business requirements into manageable parts.
2. DDD Architecture
2.1 Layered Architecture
Strict layering: a layer can only depend on the layer directly below it. Loose layering: a layer may depend on any lower layer.
DDD adopts loose layering; each layer can use services from any lower layer, allowing more flexible interactions.
Layer responsibilities (top to bottom):
User Interaction Layer : Handles web, RPC, MQ requests that may modify internal data.
Business Application Layer : Orchestrates, forwards, and validates requests; differs from MVC service which contains business logic.
Domain Layer : Core model expressing business concepts, state, and rules (entities, value objects, aggregates, services, events, repositories, factories).
Infrastructure Layer : Provides persistence mechanisms and technical support such as messaging, utilities, and configuration.
Avoid placing domain logic in the application layer; otherwise the application layer becomes a “fat” layer and the architecture degrades back to traditional three‑tier MVC.
2.2 Data Transformation per Layer
Each layer uses specific data objects:
VO (View Object) : Data for UI presentation.
DTO (Data Transfer Object) : Used for remote calls to transfer only needed fields, reducing exposure of internal structures.
DO (Domain Object) : Business entity abstracted from the real world.
PO (Persistent Object) : Direct mapping to database tables.
3. DDD Fundamentals
Key concepts include value, attribute, identifier (basic data units), entities (encapsulated data objects), domain layers (order, product, payment, etc.), and application services (business orchestration).
3.1 Domain and Subdomain
DDD divides a business domain into subdomains, each representing a bounded problem space. Subdomains can be further split into smaller sub‑domains, reducing complexity and focusing design effort.
3.2 Core, Generic, and Supporting Domains
Core Domain : Determines the product’s competitive advantage.
Generic Domain : Common functionality used by many subdomains.
Supporting Domain : Neither core nor generic; auxiliary functions.
3.3 Ubiquitous Language and Bounded Context
Ubiquitous Language : A clear, shared vocabulary for developers and domain experts.
Bounded Context : An explicit boundary where the ubiquitous language has a precise meaning, preventing ambiguity across contexts.
3.4 Entity and Value Object
Entity = unique identifier + mutable state + behavior. Entities are represented as DOs and retain identity across changes.
Value Object = immutable representation of a concept without identity. Example: an address serialized as JSON stored in a single field.
3.5 Aggregate and Aggregate Root
An Aggregate groups tightly related entities and value objects, defining a consistency boundary. The Aggregate Root is the entry point for external access and manages internal coordination.
3.6 Domain Service and Application Service
Domain Service contains logic that does not naturally belong to a single entity. It should be minimal; otherwise the design drifts back to a service‑heavy MVC.
Application Service orchestrates use cases, delegates business work to domain objects, and may handle cross‑cutting concerns such as authentication, transaction control, and event publishing.
3.7 Domain Events
Domain events capture significant occurrences within the domain: publishing, storing, distributing, and handling.
<code>public DomainEventPublisherImpl implements DomainEventPublisher {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void publishEvent(BaseDomainEvent event) {
log.debug("Publish event, event:{}", GsonUtil.gsonToString(event));
applicationEventPublisher.publishEvent(event);
}
}</code>3.8 Repository (Resource Library)
Repositories mediate between domain models and persistence, handling aggregate storage and retrieval.
<code>public interface UserRepository {
void delete(Long userId);
AuthorizeDO query(Long userId);
AuthorizeDO save(AuthorizeDO user);
}</code>4. DDD Practice
4.1 Project Overview
Focus on user, role, and their relationship to build an authorization domain model.
Four‑layer DDD architecture: interface, application, domain, infrastructure.
Data conversion among VO, DTO, DO, PO.
Implemented with SpringBoot, MyBatis‑Plus, MySQL.
4.2 Project Structure
<code>./ddd-application // application layer
├── pom.xml
└── src/main/java/com/ddd/applicaiton
├── converter/UserApplicationConverter.java
└── impl/AuthrizeApplicationServiceImpl.java
... (other modules omitted for brevity)</code>4.3 Database
<code>create table t_user (
id bigint auto_increment primary key comment 'Primary key',
user_name varchar(64) null comment 'Username',
password varchar(255) null comment 'Password',
real_name varchar(64) null comment 'Real name',
phone bigint null comment 'Phone',
province varchar(64) null comment 'Province',
city varchar(64) null comment 'City',
county varchar(64) null comment 'County',
unit_id bigint null comment 'Unit ID',
unit_name varchar(64) null comment 'Unit name',
gmt_create datetime default CURRENT_TIMESTAMP not null comment 'Create time',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment 'Update time',
deleted bigint default 0 not null comment 'Logical delete flag'
) comment 'User table' collate = utf8_bin;
create table t_role (
id bigint auto_increment primary key comment 'Primary key',
name varchar(256) not null comment 'Role name',
code varchar(64) null comment 'Role code',
gmt_create datetime default CURRENT_TIMESTAMP not null comment 'Create time',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment 'Update time',
deleted bigint default 0 not null comment 'Logical delete flag'
) comment 'Role table' charset = utf8;
create table t_user_role (
id bigint auto_increment primary key comment 'Primary key',
user_id bigint not null comment 'User ID',
role_id bigint not null comment 'Role ID',
gmt_create datetime default CURRENT_TIMESTAMP not null comment 'Create time',
gmt_modified datetime default CURRENT_TIMESTAMP not null comment 'Update time',
deleted bigint default 0 not null comment 'Logical delete flag'
) comment 'User‑role association' charset = utf8;</code>4.4 Infrastructure Layer
Repository implementation saves a user together with its roles:
<code>public AuthorizeDO save(AuthorizeDO user) {
UserPO userPo = userConverter.toUserPo(user);
if (Objects.isNull(user.getUserId())) {
userMapper.insert(userPo);
user.setUserId(userPo.getId());
} else {
userMapper.updateById(userPo);
userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId, user.getUserId()));
}
List<UserRolePO> userRolePos = userConverter.toUserRolePo(user);
userRolePos.forEach(userRoleMapper::insert);
return this.query(user.getUserId());
}</code>Repository interface:
<code>public interface UserRepository {
void delete(Long userId);
AuthorizeDO query(Long userId);
AuthorizeDO save(AuthorizeDO user);
}</code>4.5 Domain Layer
4.5.1 Aggregate & Aggregate Root
Users and roles form an aggregate; the user is the aggregate root, and its permissions are the aggregated data.
4.5.2 Domain Service
<code>@Service
public class AuthorizeDomainServiceImpl implements AuthorizeDomainService {
@Override
public void associatedUnit(AuthorizeDO authorizeDO) {
String unitName = "武汉小米"; // TODO: fetch from third‑party service
authorizeDO.getUnit().setUnitName(unitName);
}
}</code>4.5.3 Domain Events
<code>public abstract class BaseDomainEvent<T> implements Serializable {
private static final long serialVersionUID = 1465328245048581896L;
private LocalDateTime occurredOn;
private T data;
public BaseDomainEvent(T data) {
this.data = data;
this.occurredOn = LocalDateTime.now();
}
}
public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> {
public UserCreateEvent(AuthorizeDO user) { super(user); }
}</code>4.6 Application Layer
<code>@Transactional(rollbackFor = Exception.class)
public void createUserAuthorize(UserRoleDTO userRoleDTO) {
AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO);
authorizeDomainService.associatedUnit(authorizeDO);
AuthorizeDO saved = userRepository.save(authorizeDO);
domainEventPublisher.publishEvent(new UserCreateEvent(saved));
}
@Override
public UserRoleDTO queryUserAuthorize(Long userId) {
AuthorizeDO authorizeDO = userRepository.query(userId);
if (Objects.isNull(authorizeDO)) {
throw ValidationException.of("UserId is not exist.", null);
}
return userApplicationConverter.toAuthorizeDTO(authorizeDO);
}</code>4.7 Interface Layer
<code>@GetMapping("/query")
public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId) {
UserRoleDTO dto = authrizeApplicationService.queryUserAuthorize(userId);
Result<UserAuthorizeVO> result = new Result<>();
result.setData(authorizeConverter.toVO(dto));
result.setCode(BaseResult.CODE_SUCCESS);
return result;
}
@PostMapping("/save")
public Result<Object> create(@RequestBody AuthorizeCreateReq req) {
authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(req));
return Result.ok(BaseResult.INSERT_SUCCESS);
}</code>4.8 Running the Project
Create database tables using the provided init.sql file.
Adjust database configuration in application.yml.
Start the SpringBoot service.
Test the API: POST http://127.0.0.1:8087/api/user/save with JSON body containing userName, realName, phone, password, unitId, address, and role IDs.
4.9 Project Repository
<code>https://github.com/lml200701158/ddd-framework</code>5. Conclusion
DDD is not just a technology but a methodology that emphasizes shared language, bounded contexts, and careful domain modeling. While MVC offers quick start‑up, DDD shines for complex core services where long‑term maintainability outweighs initial overhead. Proper domain design before coding is essential; otherwise, projects may revert to messy MVC implementations.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.