Skip to main content

Components Overview

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


Component Comparison

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

When to Use What

Aggregate

Use when you need to:

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

Every domain has exactly one aggregate.

Saga

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.

Projector

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.

Process Manager

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.

Upcaster

Use when you need to:

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

Saga vs Projector vs Process Manager

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.


Upcaster

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.

How It Works

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

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

Example: Name Field Split

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"],
}

Configuration

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)

Key Points

  • 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

Next Steps