ZCore LogoZCore
Core concepts

Unit of Work & Deferred Events

Deep dive into atomic transaction boundaries and why domain events must be decoupled from database commits.

In distributed applications, one of the most common sources of data corruption and phantom side-effects is dispatching notifications or domain events before a database transaction is guaranteed to commit. ZCore's UnitOfWork (UoW) eliminates the "Dual-Write" problem by buffering events and coordinating atomic multi-repository boundaries.


1. The Atomic Transaction Boundary

UnitOfWork is an asynchronous context manager. Upon entering the context (__aenter__), it injects a state flag directly into the active SQLAlchemy session metadata:

# Internal lifecycle flag set by UnitOfWork
self.session.info["uow_managed"] = True

This flag signals BaseService instances to delegate commit authority. When any service method (create, update, delete) invokes its internal _safe_commit(), it inspects session.info["uow_managed"]. Detecting an active UoW, the service skips its individual commit, allowing multiple repositories to participate in a single atomic transaction.

If any exception is raised inside the async with block, UnitOfWork catches the error, calls session.rollback(), and purges the pending event queue.

No: depth > 1 Yes: depth == 0 Yes No / Exception async with UnitOfWork Set session uow_managed = True Execute Multiple Repository Mutations uow.register_event Buffered in Memory Outermost Root Scope? session.flush: Sync DB Constraints Physical DB Commit Succeeds? Dispatch Pending Events Concurrently session.rollback & Purge Events

2. Nested & Re-entrant Coordination (Depth Tracking)

In modular monolithic architectures, domain services frequently invoke each other (e.g., OrderService calling InventoryService and BillingService). If each service declares its own UnitOfWork boundary, a naive implementation would prematurely commit when the inner service finishes.

ZCore solves this with Depth-Aware Re-entrancy:

  1. Depth Counter (uow_depth): UnitOfWork.__aenter__ increments session.info["uow_depth"].
  2. Nested Flushes: When an inner scope (depth > 1) exits, uow.commit() executes await session.flush() instead of a physical commit. This assigns database-generated IDs, validates foreign key constraints, and synchronizes session state without ending the physical database transaction.
  3. Shared Event Buffer (uow_events): Domain events registered at any nesting depth are buffered in a shared list at session.info["uow_events"].
  4. Root Commit & Atomic Dispatch: Only when the outermost context manager (depth == 0) concludes does ZCore execute the physical database commit and dispatch all accumulated domain events concurrently via EventDispatcher.
  5. Chain Rollback: If an exception is raised at any depth, the active depth resets to 0, uow_managed is cleared, all pending events are discarded, and the entire multi-service transaction is rolled back immediately.

3. Deferred Event Dispatching (Solving the Dual-Write Problem)

Standard event dispatchers trigger listener callbacks immediately when dispatch() is called. In transactional systems, this creates severe consistency bugs:

The Premature Event Trap:

  1. db.flush() (Order inserted into session)
  2. dispatcher.dispatch("order.completed") (Sends order confirmation email)
  3. db.commit() (Fails due to a serialization or unique constraint error!)

The Bug: The database transaction rolled back (no order exists), but the customer already received a confirmation email!

The ZCore Solution

Inside a UnitOfWork, calling uow.register_event("order.completed", payload) does not execute listeners immediately. Instead, it buffers the event tuple in the session's pending events store:

async with UnitOfWork(self.repository.db, self.dispatcher) as uow:
    order = await self.order_repo.create(order_data)
    await self.inventory_repo.decrement_stock(product_id, quantity)

    # Buffered safely: Not dispatched yet!
    uow.register_event("order.completed", {"order_id": str(order.id)})
    
# Dispatches ONLY after the physical database commit succeeds on root exit!

Only after await self.session.commit() completes successfully does the UoW pop each event from _pending_events and invoke self.dispatcher.dispatch(...). If the commit fails, all buffered events are permanently discarded.

This provides absolute transactional consistency: If a domain event fires, the database state is guaranteed to be committed.


4. Integration with BaseRepository

Because all repository instances resolve the active, request-scoped AsyncSession managed by ScopedDependencyMiddleware, they automatically participate in the UnitOfWork lifecycle without any custom configuration. You can orchestrate updates across multiple independent domain repositories within a single atomic UoW block.

Handler Failure Isolation: During post-commit event dispatching, if an individual async listener raises an unhandled exception (e.g., an external webhook timeout), UnitOfWork catches and logs the failure with full traceback diagnostics, ensuring remaining event handlers continue to execute without interrupting the request lifecycle.

On this page