Components Overview
⍼ Angzarr provides five component types for building event-sourced systems. Each serves a distinct role in the CQRS/ES architecture.
Component Comparison
Section titled “Component Comparison”| Component | Input | Output | State | Use Case |
|---|---|---|---|---|
| Aggregate | Commands | Events | Via events | Domain logic, consistency boundary |
| Saga | Events (single domain) | Commands | Stateless | Cross-domain translation |
| Projector | Events (single domain preferred) | Side effects | Local only | Read models, external systems |
| Process Manager | Events (multi-domain) | Commands | Own event stream | Stateful multi-domain workflows |
| Upcaster | Old events | New events | None | Schema evolution |
When to Use What
Section titled “When to Use What”Aggregate
Section titled “Aggregate”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.
Projector
Section titled “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
Section titled “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
Section titled “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
Section titled “Saga vs Projector vs Process Manager”| Aspect | Saga | Projector | Process Manager |
|---|---|---|---|
| Output | Commands | Side effects | Commands |
| State | None | Local (in-memory) | Own event stream |
| Domain subscription | Single | Single (prefer) | Multiple |
| Receives correlation ID | Yes (propagates) | Yes (observes) | Yes (aggregate root) |
| Failure impact | Workflow incomplete | Stale read models | Workflow incomplete |
| Timeouts | No | No | Yes |
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
Section titled “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
Section titled “How It Works”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.
Example: Name Field Split
Section titled “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
Section titled “Configuration”Enable upcasting via config or environment:
upcaster: enabled: true # Optional: separate address (defaults to client logic address) address: "localhost:50053"Or via environment variables:
ANGZARR_UPCASTER_ENABLED=trueANGZARR_UPCASTER_ADDRESS=localhost:50053(optional)
Key Points
Section titled “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
Section titled “Next Steps”- Aggregates — Command handling and event emission
- Sagas — Cross-domain coordination
- Projectors — Building read models
- Process Managers — Stateful orchestration
- Framework Projectors — Built-in operational projectors