Browser docs

Unit Of Work

Intent

When a business transaction is completed, all the updates are sent as one big unit of work to be persisted in one go to minimize database round-trips.

Explanation

Real-world example

Arms dealer has a database containing weapon information. Merchants all over the town are constantly updating this information and it causes a high load on the database server. To make the load more manageable we apply to Unit of Work pattern to send many small updates in batches.

In plain words

Unit of Work merges many small database updates in a single batch to optimize the number of round-trips.

MartinFowler.com says

Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.

Programmatic Example

Here’s the Weapon entity that is being persisted in the database.

1@Getter
2@RequiredArgsConstructor
3public class Weapon {
4    private final Integer id;
5    private final String name;
6}

The essence of the implementation is the ArmsDealer implementing the Unit of Work pattern. It maintains a map of database operations (context) that need to be done and when commit is called it applies them in a single batch.

 1public interface IUnitOfWork<T> {
 2    
 3  String INSERT = "INSERT";
 4  String DELETE = "DELETE";
 5  String MODIFY = "MODIFY";
 6
 7  void registerNew(T entity);
 8
 9  void registerModified(T entity);
10
11  void registerDeleted(T entity);
12
13  void commit();
14}
15
16@Slf4j
17@RequiredArgsConstructor
18public class ArmsDealer implements IUnitOfWork<Weapon> {
19
20    private final Map<String, List<Weapon>> context;
21    private final WeaponDatabase weaponDatabase;
22
23    @Override
24    public void registerNew(Weapon weapon) {
25        LOGGER.info("Registering {} for insert in context.", weapon.getName());
26        register(weapon, UnitActions.INSERT.getActionValue());
27    }
28
29    @Override
30    public void registerModified(Weapon weapon) {
31        LOGGER.info("Registering {} for modify in context.", weapon.getName());
32        register(weapon, UnitActions.MODIFY.getActionValue());
33
34    }
35
36    @Override
37    public void registerDeleted(Weapon weapon) {
38        LOGGER.info("Registering {} for delete in context.", weapon.getName());
39        register(weapon, UnitActions.DELETE.getActionValue());
40    }
41
42    private void register(Weapon weapon, String operation) {
43        var weaponsToOperate = context.get(operation);
44        if (weaponsToOperate == null) {
45            weaponsToOperate = new ArrayList<>();
46        }
47        weaponsToOperate.add(weapon);
48        context.put(operation, weaponsToOperate);
49    }
50
51    /**
52     * All UnitOfWork operations are batched and executed together on commit only.
53     */
54    @Override
55    public void commit() {
56        if (context == null || context.size() == 0) {
57            return;
58        }
59        LOGGER.info("Commit started");
60        if (context.containsKey(UnitActions.INSERT.getActionValue())) {
61            commitInsert();
62        }
63
64        if (context.containsKey(UnitActions.MODIFY.getActionValue())) {
65            commitModify();
66        }
67        if (context.containsKey(UnitActions.DELETE.getActionValue())) {
68            commitDelete();
69        }
70        LOGGER.info("Commit finished.");
71    }
72
73    private void commitInsert() {
74        var weaponsToBeInserted = context.get(UnitActions.INSERT.getActionValue());
75        for (var weapon : weaponsToBeInserted) {
76            LOGGER.info("Inserting a new weapon {} to sales rack.", weapon.getName());
77            weaponDatabase.insert(weapon);
78        }
79    }
80
81    private void commitModify() {
82        var modifiedWeapons = context.get(UnitActions.MODIFY.getActionValue());
83        for (var weapon : modifiedWeapons) {
84            LOGGER.info("Scheduling {} for modification work.", weapon.getName());
85            weaponDatabase.modify(weapon);
86        }
87    }
88
89    private void commitDelete() {
90        var deletedWeapons = context.get(UnitActions.DELETE.getActionValue());
91        for (var weapon : deletedWeapons) {
92            LOGGER.info("Scrapping {}.", weapon.getName());
93            weaponDatabase.delete(weapon);
94        }
95    }
96}

Here is how the whole app is put together.

 1// create some weapons
 2var enchantedHammer = new Weapon(1, "enchanted hammer");
 3var brokenGreatSword = new Weapon(2, "broken great sword");
 4var silverTrident = new Weapon(3, "silver trident");
 5
 6// create repository
 7var weaponRepository = new ArmsDealer(new HashMap<String, List<Weapon>>(), new WeaponDatabase());
 8
 9// perform operations on the weapons
10weaponRepository.registerNew(enchantedHammer);
11weaponRepository.registerModified(silverTrident);
12weaponRepository.registerDeleted(brokenGreatSword);
13weaponRepository.commit();

Here is the console output.

21:39:21.984 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering enchanted hammer for insert in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering silver trident for modify in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering broken great sword for delete in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit started
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Inserting a new weapon enchanted hammer to sales rack.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scheduling silver trident for modification work.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scrapping broken great sword.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit finished.

Class diagram

alt text

Applicability

Use the Unit Of Work pattern when

  • To optimize the time taken for database transactions.
  • To send changes to database as a unit of work which ensures atomicity of the transaction.
  • To reduce the number of database calls.

Tutorials

Credits