Understanding Aggregates, Entities, and Value Objects in Domain-Driven Design
The article explains DDD’s core building blocks—entities identified by persistent IDs, immutable value objects, and aggregates that group related objects under a single root—using a car‑model case study to show how roots enforce invariants, how factories, repositories and lazy‑loaded references fit within a hexagonal architecture, and why these practices improve modular, maintainable software.
This article is the third part of the "Domain‑Driven Design (DDD) Practice Road" series. It focuses on the design of aggregates, explains why incorrect aggregate models are common, and provides a step‑by‑step analysis of the concepts of Entity, Value Object, and Aggregate.
Entity vs. Value Object
An Entity is identified by a globally unique identity that persists throughout its lifecycle, even when its attributes change. Entities are usually persisted as rows in a database table. Examples include a user in an e‑commerce system, a car, a lottery ticket, or a bank transaction.
A Value Object describes immutable attributes that have no identity of their own. It is used only for measurement or description, such as an address, a color, or a monetary amount. Value objects must be immutable, can be shared, and should be replaced entirely when their state changes.
The article lists the essential characteristics of a value object:
It measures or describes a domain concept.
It is immutable.
It combines related attributes into a cohesive whole.
When its description changes, a new value object replaces the old one.
Aggregate
An Aggregate is a cluster of related domain objects treated as a single unit for data modification. It has a single root entity and a defined boundary. External objects may only hold references to the root; internal objects may reference each other freely. The root guarantees the consistency of the whole aggregate.
The article illustrates the concept with a car‑model example.
Case Study: Car Model Design
Constraints:
A car belongs to a single owner (person or enterprise).
A car has four wheels and one engine.
In this model, Car is the aggregate root, and Wheel , Engine , and Customer are entities inside the aggregate.
public class Car {
private Customer customer;
/**
* WheelPositionEnum enumerates wheel positions: FR, FL, BR, BL.
* Wheels are kept independent inside the aggregate.
*/
private Map<String, Wheel> wheels;
private Engine engine;
// other attributes omitted
}Business‑rule enforcement should be encapsulated in the root entity. The article shows a naïve implementation that checks constraints in a service method and then proposes a domain‑driven approach where the Car itself validates its state.
public Car getCar(Long id) {
Car car = carRepository.ofId(id);
if (car.getEngine() == null || car.getWheels().keySet().size() != SPECIFIC_WHEEL_SIZE) {
throw new CarStatusException(id);
}
return car;
}
/**
* Better: let Car encapsulate its own invariants.
*/
public Car getWorkableCar(Long id) {
Car car = carRepository.ofId(id);
if (!car.workable()) {
throw new CarStatusException(id);
}
return car;
}To avoid performance problems caused by loading the whole aggregate, the engine can be extracted into a separate aggregate and referenced by engineId (lazy loading).
public class Car {
private Customer customer;
private Map
wheels;
// Lazy‑loaded engine reference
private String engineId;
// ...
}Factory, Repository, and Hexagonal Architecture
Complex aggregate creation should be hidden behind a Factory so that callers do not need to know internal construction details. Repositories provide a global access point for aggregate roots, while the Hexagonal Architecture separates the domain core from input/output ports.
Example of a facade that converts a Car aggregate into a view object for the presentation layer:
// RPC call to obtain Car aggregate data
CarData carData = carFacade.OfId(carId);
CarVO carVO = CarVOFactory.build(carData.getValue());Domain services are introduced for operations that do not naturally belong to any entity or value object, such as authentication or complex business processes.
// AuthenticationService registration example
UserDescriptor userDescriptor = DomainRegistry.authenticationService()
.authenticate(userId, password);The article also discusses naming conventions, the distinction between behavior and strategy, and the importance of keeping the domain model stable while allowing extensions through strategies (e.g., navigation strategies).
Finally, the article emphasizes that even in non‑DDD projects, the practices around aggregates—clear boundaries, lazy loading, factories, repositories, and domain services—provide valuable guidance for building flexible, maintainable software.
Reference:
Domain‑Driven Design: Tackling Complexity in the Heart of Software
Implementing Domain‑Driven Design
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
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.