Stackbook Logo
data-consistencyestablished · low operational burden

Unit of Work

Also known as: uow, transaction-scope

Intent

Track changes made during a business operation and commit them together as a single atomic transaction.

Problem

When each repository call commits on its own, a use case that changes several objects can fail halfway and leave inconsistent data. Developers also repeat transaction handling in every operation.

Forces

  • •A business operation must succeed or fail as a whole
  • •Transaction handling should not clutter domain logic
  • •Database round trips should be minimized
  • •Long-lived units hold locks and memory

Solution

Overview

Martin Fowler describes a unit of work as maintaining a list of objects affected by a business transaction and coordinating the writing out of changes. Most ORMs, such as Hibernate sessions, Entity Framework contexts, and SQLAlchemy sessions, implement it internally.

How it works

The application service opens a unit, loads aggregates through repositories, calls domain methods, and commits.

with uow:                       # begins transaction
    order = uow.orders.get(order_id)
    order.confirm()
    uow.stock.reserve(order.lines)
    uow.commit()                # flush all changes atomically
# exiting without commit rolls back
begin -> load -> mutate -> commit  (all writes in one DB transaction)
                    \-> error -> rollback

Domain events raised by aggregates can be collected by the unit and dispatched only after a successful commit, or written to an outbox table in the same transaction.

Pitfalls

  • Making a network call to another service inside the unit; if it hangs, locks stay held.
  • Scoping the unit to the whole HTTP session or user conversation.
  • Assuming it gives atomicity across a database and a message broker; use the outbox pattern for that.
  • Calling commit inside repositories, which defeats the pattern.
  • Not handling optimistic concurrency exceptions with a retry strategy.

Key takeaways

  • One unit of work per use case, committed once.
  • Keep it short and local to a single database.
  • Collect events and publish after commit.
  • For cross-service consistency use sagas, not larger units.

✓ When to Use

  • •Use cases touching several repositories in one database
  • •When using an ORM with change tracking
  • •To collect domain events and publish after commit

✗ When Not to Use

  • •Single-statement operations
  • •Across independent services, which need sagas rather than local transactions
  • •Stateless, read-only queries

Pros

  • +Atomic multi-object changes
  • +Single place for transaction boundaries
  • +Batching of writes and fewer round trips

Cons

  • −Hidden magic in change tracking
  • −Long units cause lock contention and stale state
  • −Does not span multiple databases or services

Cost Profile

Infrastructure

None — uses database transactions

Operational

Low — watch transaction duration

Cognitive

Medium — session and tracking semantics

Failure Modes

  • Unit spanning remote calls and holding locks while waiting

  • Forgetting to commit, silently dropping changes

  • Reusing a unit across requests and leaking stale tracked entities

Real-World Examples

Alternatives

    Related Patterns

      Competency Domains

      data statereliability ops