How to Implement the Unit of Work Pattern for Transaction Management in Java
The Unit of Work pattern groups multiple database operations into a single atomic transaction that executes only when you explicitly call commit(), ensuring data consistency by delaying persistence until all business rules are validated.
The Unit of Work pattern is essential for maintaining data integrity when multiple related operations must succeed or fail together. In the iluwatar/java-design-patterns repository, this pattern demonstrates how to accumulate inserts, updates, and deletes in a context object before flushing them to the database as one transactional unit.
Core Components of the Unit of Work Pattern
The implementation consists of five collaborating classes that separate transaction coordination from persistence logic.
The UnitOfWork Interface
The UnitOfWork<T> interface in unit-of-work/src/main/java/com/iluwatar/unitofwork/UnitOfWork.java defines the contract for any transactional repository. It declares three registration methods and a commit operation:
registerNew(T entity)– queues an insert operationregisterModified(T entity)– queues an update operationregisterDeleted(T entity)– queues a delete operationcommit()– executes all queued operations atomically
UnitActions Enum
Located at unit-of-work/src/main/java/com/iluwatar/unitofwork/UnitActions.java, this enum defines the three possible actions: INSERT, MODIFY, and DELETE. These values serve as keys in the context map to categorize pending operations.
ArmsDealer Concrete Implementation
The ArmsDealer class in unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java provides the concrete implementation of UnitOfWork<Weapon>. It maintains a Map<String, List<Weapon>> context where keys correspond to UnitActions values and values are lists of entities awaiting that specific operation.
When the client calls registration methods, the implementation stores entities in this context map without touching the database. The actual persistence logic resides in three private helper methods: commitInsert(), commitModify(), and commitDelete().
How the Transaction Boundary Works
The transaction boundary is enforced entirely within the commit() method implementation. When invoked, the repository follows this sequence:
- Validation: Checks that the context map contains pending operations.
- Insertion Phase: Iterates over the list mapped to
INSERTand callsWeaponDatabase.insert()for each entity. - Modification Phase: Processes the
MODIFYlist by delegating toWeaponDatabase.modify(). - Deletion Phase: Processes the
DELETElist viaWeaponDatabase.delete(). - Cleanup: Clears the context map to prepare for the next transaction unit.
Because all database calls occur inside this single commit() invocation, you achieve atomicity—if any operation fails, you can catch the exception and prevent partial updates (rollback logic can be added by wrapping the phases in try-catch blocks).
Practical Implementation Example
The following example demonstrates how a client coordinates multiple operations into one transactional unit:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.iluwatar.unitofwork.ArmsDealer;
import com.iluwatar.unitofwork.Weapon;
import com.iluwatar.unitofwork.WeaponDatabase;
public class TransactionExample {
public static void main(String[] args) {
// 1. Initialize the repository with an empty context
Map<String, List<Weapon>> context = new HashMap<>();
WeaponDatabase db = new WeaponDatabase();
ArmsDealer weaponRepo = new ArmsDealer(context, db);
// 2. Create domain objects
Weapon hammer = new Weapon(1, "Enchanted Hammer");
Weapon sword = new Weapon(2, "Broken Great Sword");
Weapon trident = new Weapon(3, "Silver Trident");
// 3. Register operations (no database calls yet)
weaponRepo.registerNew(hammer); // Queued for INSERT
weaponRepo.registerModified(trident); // Queued for MODIFY
weaponRepo.registerDeleted(sword); // Queued for DELETE
// 4. Atomic commit - all operations execute here
weaponRepo.commit();
}
}
In this flow, registerNew(), registerModified(), and registerDeleted() merely populate the internal context map. The actual database operations defined in unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java are invoked only when commit() runs, ensuring that the enchanted hammer insertion, trident modification, and sword deletion either all succeed or all fail together.
Adapting the Pattern for Other Entities
To reuse this Unit of Work implementation for a different domain object such as Order, you need three artifacts:
- Domain Entity: Create
Order.javawith relevant fields. - Database Façade: Implement
OrderDatabase.javawithinsert(Order),modify(Order), anddelete(Order)methods. - Concrete Repository: Create
OrderRepository.javaimplementingUnitOfWork<Order>, mirroring theArmsDealerstructure but typed forOrder.
For larger applications, extract the common context management logic into an AbstractUnitOfWork<T> base class that concrete repositories can extend, reducing boilerplate while preserving the transaction semantics.
Summary
- Unit of Work delays all database operations until
commit()is called, creating a clear transaction boundary. - The ArmsDealer class in
unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.javauses aMap<String, List<Weapon>>context to track pending inserts, modifications, and deletes. - UnitActions enum values serve as keys to categorize operations in the context map.
- All persistence logic executes atomically inside the
commit()method, which delegates toWeaponDatabasefor actual database interaction. - The pattern separates business logic registration from transaction execution, making code more testable and maintainable.
Frequently Asked Questions
What is the primary benefit of using the Unit of Work pattern?
The primary benefit is atomicity. By buffering all changes in memory and executing them during a single commit() call, you ensure that complex business operations involving multiple inserts, updates, and deletes either complete entirely or fail without leaving the database in an inconsistent state. This eliminates partial updates that can corrupt data integrity.
How does the ArmsDealer class track pending database operations?
ArmsDealer maintains a private Map<String, List<Weapon>> context field. When you call registerNew(), registerModified(), or registerDeleted(), the method retrieves the appropriate list using the corresponding UnitActions enum value as the key, then adds the entity to that list. No database interaction occurs during registration; the map simply accumulates references to entities awaiting persistence.
Can the Unit of Work pattern handle transaction rollback?
While the reference implementation in the java-design-patterns repository focuses on the commit flow, you can extend ArmsDealer to support rollback by wrapping the commitInsert(), commitModify(), and commitDelete() calls in a try-catch block. If any phase throws an exception, catch it and implement compensating transactions or database rollback logic to revert previously applied changes within that commit scope.
How do I implement this pattern for entity types other than Weapon?
Implement the UnitOfWork<T> interface with a concrete class tailored to your entity type. Replace Weapon with your domain class (e.g., Order), create a corresponding database façade (e.g., OrderDatabase), and use the same Map<String, List<T>> structure for the context. The registration and commit logic remains identical, making the pattern highly reusable across different domain models.
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 →