Skip to content

Components Overview

⍼ Angzarr provides five component types for building event-sourced systems. Each serves a distinct role in the CQRS/ES architecture.


ComponentInputOutputStateUse Case
AggregateCommandsEventsVia eventsDomain logic, consistency boundary
SagaEvents (single domain)CommandsStatelessCross-domain translation
ProjectorEvents (single domain preferred)Side effectsLocal onlyRead models, external systems
Process ManagerEvents (multi-domain)CommandsOwn event streamStateful multi-domain workflows
UpcasterOld eventsNew eventsNoneSchema evolution

Use when you need to:

  • Validate commands against current state
  • Enforce business invariants
  • Maintain a consistency boundary

Every domain has exactly one aggregate.

Use when you need to:

  • Translate events from domain A into commands for domain B
  • React to events without maintaining state
  • Keep cross-domain coordination simple

Sagas subscribe to ONE domain only. For multi-domain subscription, use a Process Manager.

Use when you need to:

  • Build read models (query-optimized views)
  • Write to external systems (search, cache, analytics)

Projectors never emit commands. They only observe. Prefer single-domain projectors; multi-domain is possible but often signals incorrect domain boundaries.

Use when you need to:

  • Coordinate workflows spanning multiple domains
  • Maintain workflow state across events
  • Implement state machines with timeouts

Process Managers are their own aggregate, keyed by correlation ID.

Use when you need to:

  • Migrate event schemas without rewriting history
  • Transform old event versions on read
  • Evolve your domain model gradually

AspectSagaProjectorProcess Manager
OutputCommandsSide effectsCommands
StateNoneLocal (in-memory)Own event stream
Domain subscriptionSingleSingle (prefer)Multiple
Receives correlation IDYes (propagates)Yes (observes)Yes (aggregate root)
Failure impactWorkflow incompleteStale read modelsWorkflow incomplete
TimeoutsNoNoYes

Rule of thumb: Start with sagas. Upgrade to Process Manager when you need state tracking or multi-domain input. Use projectors for read models.


Upcasters handle schema management by transforming old event versions to current versions when reading from the event store. This enables schema evolution without rewriting historical events.

Upcasters run in the aggregate pod, alongside your aggregate logic. The coordinator orchestrates the transformation:

sequenceDiagram
    participant ES as Event Store
    participant C as ⍼ Coordinator
    participant U as Your Upcaster
    participant A as Your Aggregate

    C->>ES: Read EventBook
    ES->>C: EventPage (V1)
    C->>U: Transform request
    U->>C: EventPage (V2)
    C->>A: Handle command with V2 events

The stored event remains unchanged (V1). The upcaster transforms it to V2 on read, so your aggregate only sees current-version events.

V1 events had a single name field. V2 splits into first_name and last_name:

from angzarr_client import Upcaster
class PlayerRegisteredV1ToV2(Upcaster):
def can_upcast(self, event_type: str, version: int) -> bool:
return event_type == "PlayerRegistered" and version == 1
def upcast(self, event: dict) -> dict:
name_parts = event["name"].split(" ", 1)
return {
"first_name": name_parts[0],
"last_name": name_parts[1] if len(name_parts) > 1 else "",
"email": event["email"],
}

Enable upcasting via config or environment:

illustrative - upcaster configuration
upcaster:
enabled: true
# Optional: separate address (defaults to client logic address)
address: "localhost:50053"

Or via environment variables:

  • ANGZARR_UPCASTER_ENABLED=true
  • ANGZARR_UPCASTER_ADDRESS=localhost:50053 (optional)
  • Stored events remain unchanged — immutability preserved
  • Transformation happens on read — lazy migration
  • Chain upcasters for multi-version jumps (V1 → V2 → V3)
  • Upcasters run in the aggregate pod — potentially separate gRPC server from your aggregate