How IUnitOfWork Manages the Lifecycle of Domain Objects in DDDplus
In DDDplus, IUnitOfWork acts as an Application Layer coordination hub that atomically persists aggregates across different roots, ensuring domain objects transition from transient to persistent state within a single transactional boundary.
The funkygao/cp-ddd-framework repository implements the Unit of Work pattern to enforce strict separation between domain logic and infrastructure concerns. By defining a clear semantic contract through the IUnitOfWork interface, the framework enables application services to coordinate complex persistence operations spanning multiple aggregate roots while remaining agnostic to transaction management details.
The Marker Interface Pattern
At its core, IUnitOfWork is a marker interface that declares participation in the unit-of-work pattern without imposing implementation constraints. Defined in dddplus-spec/src/main/java/io/github/dddplus/model/IUnitOfWork.java, this empty interface serves as a semantic signal: any class implementing it commits to coordinating persistence operations atomically.
This design allows the domain layer to remain pure while the application layer handles cross-cutting infrastructure concerns. The interface itself contains no methods, relying on concrete implementations to provide specific persist(..) overloads for different aggregate combinations.
Cross-Aggregate Transaction Coordination
Real-world business use cases frequently touch multiple aggregate roots simultaneously—such as a Task, Carton, and Order in warehouse management scenarios. IUnitOfWork establishes a transaction boundary that guarantees atomicity across these distinct aggregates, preventing partial persistence failures that would violate domain invariants.
The concrete implementation in dddplus-test/src/test/java/ddd/plus/showcase/wms/application/UnitOfWork.java demonstrates this coordination:
@Component
@Setter(onMethod_ = {@Autowired})
@Slf4j
public class UnitOfWork implements IUnitOfWork {
private ITaskRepository taskRepository;
private IOrderRepository orderRepository;
private ICartonRepository cartonRepository;
private IShipRepository shipRepository;
@Transactional(rollbackFor = Exception.class)
public void persist(@NonNull Task task, @NonNull Carton carton) {
taskRepository.save(task); // persists Task aggregate
cartonRepository.save(carton); // persists Carton aggregate
}
}
Each persist(..) method variant handles specific aggregate combinations, ensuring that all participating repositories—including ITaskRepository and ICartonRepository—commit or rollback together under Spring's transaction management.
Spring-Managed Transaction Lifecycle
The UnitOfWork implementation is a Spring-managed @Component that delegates low-level transaction handling to the container. Every persistence method carries the @Transactional(rollbackFor = Exception.class) annotation, which instructs Spring to:
- Open a database transaction upon method entry
- Execute repository operations such as
save,insert, orswitchToCanceledStatus - Automatically commit on successful return or rollback on exception propagation
This infrastructure abstraction keeps domain logic free from EntityManager or JDBC transaction APIs. Application services inject the IUnitOfWork interface and invoke persistence operations without managing transaction boundaries manually.
Lifecycle State Transitions
Within each transactional scope, UnitOfWork orchestrates the transition of aggregates from new or modified states to persistent states. The implementation forwards calls to repository interfaces—such as ICartonRepository defined in dddplus-test/src/test/java/ddd/plus/showcase/wms/domain/carton/ICartonRepository.java—which materialize the aggregate state into database tables.
Because repository operations execute within the transaction initiated by UnitOfWork, aggregates maintain consistency even when persistence spans multiple physical tables or schema boundaries. The unit of work tracks which aggregates require persistence and coordinates their transition to persistent identity together.
Idempotence and Guard Checks
Before delegating to repositories, specific persist overloads enforce domain invariants and idempotence requirements. For example, when handling Task and OrderBagCanceled combinations, the implementation invokes uuid.assureVaryOnce() to prevent duplicate processing of the same business event.
These guard checks execute within the transactional boundary but strictly before any repository interaction. This sequencing ensures that only valid state changes reach the database, while maintaining atomicity between validation and persistence.
Practical Implementation in Application Services
Application services consume IUnitOfWork through constructor injection, casting to the concrete implementation to access type-specific persist methods:
@Component
@RequiredArgsConstructor
public class ShipOrderUseCase {
private final IUnitOfWork unitOfWork; // injected concrete UnitOfWork
private final IOrderRepository orderRepo;
private final IShipRepository shipRepo;
@Transactional
public void execute(@NonNull Order order, @NonNull ShipManifest manifest) {
// business validation omitted for brevity
((UnitOfWork) unitOfWork).persist(manifest, order);
// No explicit commit needed – Spring commits at method exit
}
}
This pattern allows services to remain testable through the IUnitOfWork interface while leveraging the specific coordination logic implemented in UnitOfWork.
Summary
- IUnitOfWork is a marker interface in
dddplus-specthat defines the semantic contract for unit-of-work participation without implementation coupling. - The concrete UnitOfWork class in the application layer coordinates persistence across multiple aggregate roots (e.g.,
Task,Carton,Order) within a single atomic transaction. - Spring's
@Transactionalannotation onpersist(..)methods provides automatic commit/rollback behavior, separating transaction infrastructure from domain logic. - Repository interfaces such as
ITaskRepositoryexecute within this boundary to transition aggregates from transient to persistent states consistently. - Guard checks like
uuid.assureVaryOnce()enforce idempotence and domain invariants before database interaction occurs.
Frequently Asked Questions
What distinguishes IUnitOfWork from the concrete UnitOfWork implementation?
IUnitOfWork is a marker interface located in dddplus-spec/src/main/java/io/github/dddplus/model/IUnitOfWork.java that establishes the semantic contract for unit-of-work participation. The concrete UnitOfWork class implements this interface and provides Spring-managed transactional coordination, located in dddplus-test/src/test/java/ddd/plus/showcase/wms/application/UnitOfWork.java.
How does IUnitOfWork ensure atomicity across multiple aggregates?
The concrete UnitOfWork implementation uses Spring's @Transactional(rollbackFor = Exception.class) annotation on its persist(..) methods. When an application service invokes these methods, Spring opens a transaction that encompasses all repository calls (such as taskRepository.save() and cartonRepository.save()), ensuring they commit or rollback as a single unit.
Can IUnitOfWork manage the lifecycle of a single aggregate?
While IUnitOfWork excels at coordinating multiple aggregates, it supports single-aggregate persistence through specific method overloads. However, direct repository usage is typically preferred for single-aggregate operations unless the additional guard checks and idempotence validation provided by UnitOfWork are required.
Where should domain validation occur in the persistence lifecycle?
Guard checks belong in the concrete UnitOfWork implementation before repository delegation. According to the DDDplus source code, validations such as uuid.assureVaryOnce() execute within the transactional scope but prior to any repository save operations, ensuring invalid states never reach the database while maintaining atomicity with persistence.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →