Architecture
This document covers ⍼ Angzarr’s core architectural concepts: event sourcing data model, coordinator pattern, deployment model, and synchronization modes.
Event Sourcing Data Model
Section titled “Event Sourcing Data Model”⍼ Angzarr stores aggregate history as an EventBook—the complete event stream for a single aggregate root:
| Component | Purpose |
|---|---|
| Cover | Identity: domain, aggregate root ID, correlation ID |
| Snapshot | Point-in-time state for replay optimization |
| EventPages | Ordered sequence of domain events |
message Cover { string domain = 2; UUID root = 1; string correlation_id = 3; // Workflow correlation - flows through all commands/events Edition edition = 4; // Edition for diverged timelines; empty name = main timeline // Field 5 removed: external_id moved to ExternalDeferredSequence in PageHeader // Client-supplied extension slot. Most consumers pack a nested // ``Cover`` here to express parent-aggregate relationships (e.g. // table events carry the tournament Cover; hand events carry the // table Cover). Helper ``unpack_parent_cover`` extracts the typed // Cover when present. Framework code treats this as opaque routing // metadata — it propagates without inspecting. // // WARNING — mass-propagating: the framework stamps this slot onto // EVERY event a child aggregate emits, so the payload is paid per // event. Keep what you pack small. The intended workload is a single // nested Cover (~50–80 bytes packed); larger payloads multiply // across the event stream and the bus. google.protobuf.Any ext = 6;}message EventPage { PageHeader header = 1; // Sequence type and provenance google.protobuf.Timestamp created_at = 2; oneof payload { google.protobuf.Any event = 3; PayloadReference external = 4; // Claim check: payload stored externally } // Field 5 removed: FactSequence replaced by PageHeader.external_deferred
// Two-phase commit support (Phase 1: 2PC Storage Model) bool no_commit = 6; // true = pending 2PC (cascade), needs Confirmation; false (default) = immediately committed optional string cascade_id = 7; // Groups related pending events for atomic commit/rollback (null if not in cascade)}// Snapshot of aggregate state at a given sequence number.// State must be a protobuf Message to serialize into Any.message Snapshot { uint32 sequence = 2; google.protobuf.Any state = 3; SnapshotRetention retention = 4; // Controls cleanup behavior // Wall-clock timestamp the snapshot was persisted. Used by // temporal-by-time queries to decide whether the snapshot's // event coverage predates the target timestamp (snapshot is safe // to use iff created_at <= target). Absent on snapshots persisted // before this field existed; readers MUST treat absent as // "do not use this snapshot for temporal-by-time" (safe full-replay // fallback). google.protobuf.Timestamp created_at = 5;}message EventBook { Cover cover = 1; Snapshot snapshot = 2; // Snapshot state; sequence computed by framework on persist repeated EventPage pages = 3; // Field 4 removed: correlation_id moved to Cover // Field 5 removed: snapshot_state unified into snapshot field uint32 next_sequence = 6; // Computed on load, never stored: (last page seq OR snapshot seq if no pages) + 1}Commands follow the same pattern—a CommandBook contains one or more CommandPages targeting a single aggregate.
Coordinator Pattern
Section titled “Coordinator Pattern”⍼ Angzarr uses coordinators to route messages between external clients and your business logic. This separation keeps domain code focused while the framework handles:
- Event persistence and retrieval
- Optimistic concurrency via sequence numbers
- Snapshot management
- Event upcasting (schema evolution on read)
- Synchronous vs. asynchronous processing
Component Types
Section titled “Component Types”| Coordinator | Routes | Purpose |
|---|---|---|
| CommandHandlerCoordinator | Commands → Aggregates | Command handling, event persistence |
| ProjectorCoordinator | Events → Projectors | Read model updates, side effects |
| SagaCoordinator | Events → Sagas | Cross-domain command orchestration |
| ProcessManagerCoordinator | Events → PMs | Stateful multi-domain workflows |
flowchart TB
Client[External Client]
CHC[CommandHandlerCoordinator]
Agg[Your Aggregate]
ES[(Event Store)]
BUS[Message Bus]
SC[SagaCoordinator]
PC[ProjectorCoordinator]
Saga[Your Saga]
Proj[Your Projector]
RM[(Read Model<br/>Postgres, Redis)]
Client -->|gRPC| CHC
CHC <-->|gRPC| Agg
CHC -->|SQL| ES
CHC -->|AMQP/Kafka| BUS
BUS -->|AMQP/Kafka| SC
BUS -->|AMQP/Kafka| PC
SC <-->|gRPC| Saga
SC -->|gRPC| CHC
PC <-->|gRPC| Proj
Proj -->|SQL| RM
Sidecar Deployment
Section titled “Sidecar Deployment”⍼ Angzarr runs as a sidecar container alongside your business logic. Each pod contains your code and the appropriate coordinator, communicating over localhost gRPC.
flowchart TB
subgraph AggPod[Aggregate Pod]
subgraph AggCode[Your Code]
Agg[Aggregate Logic]
Upc[Upcaster Logic]
end
AggSidecar[⍼ Command Handler Coordinator]
AggCode <-->|gRPC| AggSidecar
end
subgraph SagaPod[Saga Pod]
Saga[Saga Logic]
SagaSidecar[⍼ Saga Coordinator]
Saga <-->|gRPC| SagaSidecar
end
subgraph PrjPod[Projector Pod]
Prj[Projector Logic]
PrjSidecar[⍼ Projector Coordinator]
Prj <-->|gRPC| PrjSidecar
end
AggSidecar -->|SQL| ES[(Event Store)]
AggSidecar -->|AMQP/Kafka| MB[Message Bus]
MB -->|AMQP/Kafka| SagaSidecar
MB -->|AMQP/Kafka| PrjSidecar
SagaSidecar -->|gRPC| AggSidecar
PrjSidecar -.->|gRPC| AggSidecar
Prj -->|SQL| PrjDB[(Projection DB)]
Benefits
Section titled “Benefits”- Minimal attack surface: ~8MB distroless container, no shell, no package manager
- No network exposure: Sidecar communicates over localhost only
- Horizontal scaling: Follows your service scaling—no separate capacity planning
- Local gRPC: Eliminates network latency between logic and framework
Synchronization Modes
Section titled “Synchronization Modes”⍼ Angzarr provides mechanisms for controlling sync vs async communication — when results return to callers.
SyncMode
Section titled “SyncMode”// Controls synchronous processing behavior.//// Impact ordering (lowest to highest):// ASYNC < DECISION < SIMPLE < CASCADE//// Primary caller of DECISION is process managers coordinating cross-aggregate// flows: the PM issues a command to the target aggregate and needs to know// *only* whether the aggregate accepted or rejected, without paying the cost// of projector propagation or saga fan-out. The aggregate's accept path// (events persisted + returned) and reject path (CommandRejectedError) are// both observable; everything downstream runs asynchronously.enum SyncMode { SYNC_MODE_ASYNC = 0; // Async: fire and forget (default) SYNC_MODE_SIMPLE = 1; // Sync projectors only, no saga cascade SYNC_MODE_CASCADE = 2; // Full sync: projectors + saga cascade (expensive) SYNC_MODE_DECISION = 3; // Sync aggregate accept/reject only; projectors + sagas run async SYNC_MODE_ISOLATED = 4; // Sync accept/reject + persist; NO downstream (sync OR async). Replay / migration / recovery writes that must not trigger reactions. Distinct from DECISION (which still publishes to bus for async downstream).}| Mode | Projectors | Sagas | Use Case |
|---|---|---|---|
NONE | Async | Async | Fire-and-forget, eventual consistency |
SIMPLE | Sync | Async | Read-after-write for single aggregate |
CASCADE | Sync | Sync (recursive) | Synchronous cross-aggregate workflows |
The Cascade Flow
Section titled “The Cascade Flow”When sync_mode = CASCADE, the framework orchestrates the complete cascade:
flowchart TB
Client --> CHC[CommandHandlerCoordinator.Handle<br/>Domain A]
CHC --> BL[BusinessLogic.Handle → events]
BL --> Persist[Persist events]
Persist --> SC[SagaCoordinator.HandleSync]
SC --> Saga[Saga.HandleSync → commands]
Saga --> SC
SC -.->|gRPC| CHC2[CommandHandlerCoordinator.Handle<br/>Domain B]
CHC2 -.-> BL2[BusinessLogic.Handle → events]
BL2 -.-> Persist2[Persist events]
Persist --> PC[ProjectorCoordinator.HandleSync]
PC --> Resp[CommandResponse<br/>events, projections]
style CHC2 stroke-dasharray: 5 5
style BL2 stroke-dasharray: 5 5
style Persist2 stroke-dasharray: 5 5
The dashed Domain B represents the target aggregate—sagas bridge events from one domain to commands in another. The cascade recursively waits for the target aggregate’s events before returning.
Warning: CASCADE is expensive and does not provide ACID guarantees. Each step adds latency. Start with NONE, move to SIMPLE for read-after-write, reserve CASCADE for workflows requiring synchronous cross-aggregate coordination.
Event Streaming
Section titled “Event Streaming”For observing events as they happen rather than waiting:
// EventStreamService: streams events to registered subscribersservice EventStreamService { // Subscribe to events matching correlation ID (required) // Returns INVALID_ARGUMENT if correlation_id is empty // REST: Server-Sent Events (SSE) stream rpc Subscribe(EventStreamFilter) returns (stream EventBook) { option (google.api.http) = {get: "/v1/stream/{correlation_id}"}; }}Events are correlated via correlation_id on Cover, allowing clients to track causally-related events across aggregate boundaries.
Pluggable Infrastructure
Section titled “Pluggable Infrastructure”⍼ Angzarr abstracts storage and messaging behind adapter interfaces.
Event Store Backends
Section titled “Event Store Backends”| Backend | Status | Use Case |
|---|---|---|
| SQLite | Tested | Local development, testing |
| PostgreSQL | Tested | Production |
| Redis | Tested | High-throughput scenarios |
| Bigtable | Tested | GCP deployments, petabyte scale |
| DynamoDB | Tested | AWS deployments, serverless |
| immudb | Implemented | Immutable audit requirements |
Message Bus Backends
Section titled “Message Bus Backends”⍼ Angzarr uses publish/subscribe messaging to distribute events to sagas and projectors.
| Backend | Status | Use Case |
|---|---|---|
| RabbitMQ/AMQP | Tested | Production |
| Kafka | Implemented | High-throughput streaming |
| GCP Pub/Sub | Implemented | GCP deployments |
| AWS SNS/SQS | Implemented | AWS deployments |
Configuration is declarative:
# Productionstorage: type: postgres connection_string: ${DATABASE_URL}
bus: type: amqp url: ${RABBITMQ_URL}
# Local developmentstorage: type: sqlite path: ./data/events.db
bus: type: channelObservability
Section titled “Observability”Every command, saga, and projector execution is traced and metered at the coordinator level—your code requires zero observability boilerplate.
| Pipeline | Traced Spans | Metrics |
|---|---|---|
| Aggregate | aggregate.handle, aggregate.execute | angzarr.command.duration |
| Saga | saga.orchestrate, orchestration.execute | angzarr.saga.duration |
| Process Manager | pm.orchestrate | angzarr.pm.duration |
| Projector | projector.handle | angzarr.projector.duration |
Every span carries the correlation_id, so distributed traces follow commands through aggregate execution, saga fan-out, and downstream projections without manual context propagation.
When built with --features otel, the sidecar exports traces, metrics, and logs via OTLP to any compatible backend (Grafana, Datadog, AWS X-Ray, GCP Cloud Trace).
See Observability for full details.
Further Reading
Section titled “Further Reading”For visual explanations of event-driven architecture concepts (events, messaging patterns, event sourcing, choreography vs orchestration, schema management):
- EDA Visuals by @boyney123 — Bite-sized diagrams explaining EDA concepts (PDF download)
Next Steps
Section titled “Next Steps”- Components — Aggregate, saga, projector, process manager deep dives
- Getting Started — Set up your first aggregate
- Patterns — Advanced usage patterns