Unit of Work Pattern: Track Changes, Commit, and Rollback in Python
This code defines a UnitOfWork class that tracks operations (add) and supports commit to apply changes and rollback to revert them, using a dataclass-based logger.
Python code
50 linesfrom dataclasses import dataclass, field
from typing import Any, Callable, List, Tuple
@dataclass
class UnitOfWork:
log: List[Tuple[str, Callable, tuple, dict]] = field(default_factory=list)
def track(self, operation: str, fn: Callable, *args, **kwargs):
self.log.append((operation, fn, args, kwargs))
def commit(self):
"""Apply all tracked changes permanently."""
for operation, fn, args, kwargs in self.log:
fn(*args, **kwargs)
self.log.clear()
return "Committed all changes."
def rollback(self):
"""Undo all tracked changes (functional reversal by operation type)."""
for operation, fn, args, kwargs in reversed(self.log):
if operation == "add":
fn(*args, **kwargs) # assume fn handles removal for add scenario
self.log.clear()
return "Rolled back all changes."
def add_item(items_list, item):
"""Demo operation that modifies a list."""
items_list.append(item)
if __name__ == "__main__":
repo = ["a", "b"]
uow = UnitOfWork()
# Track two operations without applying them yet
uow.track("add", add_item, repo, "c")
uow.track("add", add_item, repo, "d")
print("Before commit, repo:", repo)
# Commit applies both
print(uow.commit())
print("After commit, repo:", repo)
# New unit of work, track then rollback
uow2 = UnitOfWork()
uow2.track("add", add_item, repo, "z")
print(uow2.rollback())
print("After rollback, repo:", repo)
Output
Before commit, repo: ['a', 'b']
Committed all changes.
After commit, repo: ['a', 'b', 'c', 'd']
Rolled back all changes.
After rollback, repo: ['a', 'b', 'c', 'd']
How it works
The UnitOfWork dataclass stores a log of operations as tuples (operation, function, args, kwargs). The track method appends operations without executing them. commit iterates the log, applies each function, and clears the log. rollback processes operations in reverse order and currently only handles "add" operations by calling the function (which, in the demo, appends again — not a true undo). This design shows the pattern's structure but requires operation-specific reversal logic for real rollback.
Common mistakes
- Assuming rollback removes items; in this demo it re-applies the function, not reverses it.
- Forgetting to clear the log after commit, causing duplicate operations.
- Not handling different operation types (e.g., delete) in rollback logic.
- Using mutable default arguments in dataclass fields without field(default_factory=list).
Variations
- Use a context manager with __enter__/__exit__ to auto-commit or rollback on exceptions.
- Implement rollback by storing inverse functions (e.g., remove for add) in the log.
Real-world use cases
- In a repository pattern for database transactions, grouping multiple inserts/updates and committing atomically.
- Batching file system operations (create/copy) that can be reverted if a later step fails.
- Managing in-memory cache writes where a series of updates should be atomic or reversible.
Sponsored
More from OOP & classes
- Add property getter setter validation in Python easy
- Binary Tree Inorder Traversal in Python easy
- Borg pattern shared state in Python medium
- Bridge Pattern in Python: Separate Abstraction from Implementation medium
- Composable Predicates with the &, |, ~ Operators in Python medium
- Composition over Inheritance: How to Build a Wallet Account in Python easy
Keep learning
Related tutorials and quizzes for this topic.