Technical Pitch
A schema-first CQRS/ES framework — write business logic in any gRPC language
The symbol ⍼ (U+237C) has existed in Unicode since 2002 with no defined purpose. The right angle represents the origin point—your event store. The zigzag arrow represents events cascading through your system. We gave it meaning.
Origin
In my history as a consultant software engineer and architect, I've encountered many business logic problems where traceability/audit and the ability to handle burst traffic (high load variability) are critical. The CQRS/ES pattern addresses these concerns elegantly—but I was forewarned, and have seen evolved event architectures fail in... interesting... ways.
The pattern's appeal is clear: complete audit history, temporal queries, natural separation of concerns. The implementation reality is less rosy. Teams start with clean intentions, then infrastructure complexity creeps in. Business logic becomes entangled with persistence concerns. Schema evolution becomes an afterthought. The original architectural benefits get buried under accidental complexity.
I was inspired to create a framework—not a library—for building CQRS/ES applications that handles much of the implementation complexity. The distinction matters: libraries are imported into your code, while frameworks provide the execution environment your code runs within.
The advent of managed runtimes like GCP Cloud Run and AWS Lambda—and tools like Kubernetes that enable building them—provided the path forward. These platforms enable control to be intercepted before and after user-provided business logic. Event histories (snapshots and events) can be loaded, business logic executes in isolation, and resulting events are captured, stored, and forwarded—all without the business logic needing to know how any of it works.
The payoff: business logic becomes remarkably small. A command handler receives state and a command, validates business rules, and returns events. No database connections. No message bus configuration. No retry logic. No serialization code. Just pure domain logic. The infrastructure complexity doesn't disappear; it moves to the framework where it belongs.
See for yourself—the customer aggregate (create customer, add/redeem loyalty points) is one of the simpler examples:
| Language | LOC | Implementation |
|---|---|---|
| Go | 173 | examples/go/customer/logic/ |
| Python | 209 | examples/python/customer/ |
| Rust | 303 | examples/rust/customer/src/ |
LOC counted via scripts/render_docs.py — non-blank, non-comment lines in business logic files.
The Angzarr project uses Gherkin feature files to specify business behavior—these are not a requirement for your applications. We use them to keep business rules consistent across all example implementations (Go, Python, Rust) and to provide executable specification that runs on every commit:
| Domain | Scenarios | Feature File |
|---|---|---|
| Cart | 27 | cart.feature |
| Customer | 15 | customer.feature |
| Fulfillment | 19 | fulfillment.feature |
| Inventory | 19 | inventory.feature |
| Order | 22 | order.feature |
| Product | 18 | product.feature |
| Saga Cancellation | 5 | saga_cancellation.feature |
| Saga Fulfillment | 4 | saga_fulfillment.feature |
| Saga Loyalty Earn | 4 | saga_loyalty_earn.feature |
Your acceptance testing strategy is your own. Use whatever works for your team—unit tests, integration tests, property-based testing, or yes, Cucumber/Gherkin if that fits your workflow.
The complexity that remains is inherent to distributed systems: synchronous versus asynchronous operations. When should a command wait for projections to update before responding? When should a saga fire-and-forget versus block for completion? These are domain decisions that no framework can make for you. Angzarr provides the mechanisms—synchronous projectors, blocking saga coordination—but choosing when to use them requires understanding your consistency requirements.
Raw performance for single operations will also be poor compared to direct database writes. The gRPC roundtrip, event history loading, and persistence overhead add latency that a simple INSERT cannot match. The framework pays for itself in throughput under load (burst handling, independent scaling) and in reduced development time—not in single-request latency benchmarks.
The Problem
CQRS and Event Sourcing deliver real architectural benefits: full audit history, temporal queries, independent read/write scaling, and natural alignment with domain-driven design. The implementation cost, however, remains steep.
Teams adopting CQRS/ES face a consistent set of challenges:
- Infrastructure gravity: Event stores, message buses, projection databases, and their failure modes dominate early development cycles. Business logic becomes entangled with persistence concerns.
- Schema management: Events are append-only and permanent. Schema evolution—adding fields, deprecating event types, maintaining backward compatibility across years of stored events—requires discipline that frameworks rarely enforce.
- Operational complexity: Snapshotting, projection rebuilds, idempotency, exactly-once delivery, and saga coordination demand specialized knowledge. Each concern leaks into application code.
- Language lock-in: Most CQRS/ES frameworks assume a single-language ecosystem. Organizations with mixed stacks either maintain parallel implementations or force standardization.
The result: CQRS/ES often remains confined to greenfield projects with dedicated teams, despite its suitability for complex domains.
The Angzarr Approach
Angzarr inverts the typical framework relationship. Rather than providing libraries that applications import, Angzarr provides infrastructure that applications connect to.
The Value Proposition
| You Define | You Implement | We Handle |
|---|---|---|
Commands in .proto | gRPC BusinessLogic service | Event persistence |
Events in .proto | gRPC Projector services | Optimistic concurrency |
Read models in .proto | gRPC Saga services | Snapshot management |
| Event upcasting | ||
| Event distribution | ||
| Saga coordination | ||
| Schema evolution rules |
What Angzarr Handles (So You Don't)
| Concern | Handled By |
|---|---|
| Database transactions | Angzarr |
| Optimistic concurrency | Angzarr |
| Event ordering | Angzarr |
| Retry logic | Angzarr |
| Network failures | Angzarr |
| Service discovery | Angzarr |
| Load balancing | Angzarr |
| State hydration | Angzarr |
| Snapshot management | Angzarr |
| Event upcasting | Angzarr |
| Observability | Angzarr |
| Message serialization | Protobuf |
| Schema evolution | Protobuf |
| Deployment | DevOps/Helm |
| Scaling | K8s/DevOps |
Protocol Buffers define the contract. Commands, events, queries, and read models are declared in .proto files. This schema becomes the source of truth—for serialization, validation, documentation, and cross-service compatibility. Protobuf's established rules for backward-compatible evolution apply directly to your event schema.
gRPC provides the boundary. Business logic—aggregates, command handlers, event handlers, saga orchestrators—runs as gRPC services in any supported language. The framework communicates exclusively through generated protobuf messages over gRPC. Domain code may import Angzarr client libraries to simplify development, but this is not required — the only contract is gRPC + protobuf.
The framework handles the rest. Event persistence, command routing, event distribution, snapshot storage, idempotency, and saga state management run within Angzarr's Rust core. Your business logic receives commands with full event history and emits events. Side effects stay on one side of the gRPC boundary.
The Book Metaphor
Angzarr models event-sourced aggregates as books. An EventBook contains the complete history of an aggregate root: its identity (the Cover), an optional Snapshot for efficient replay, and ordered EventPages representing individual domain events.
This metaphor provides intuitive semantics: you read a book to understand its history, append pages as events occur, and bookmark your place with snapshots.
// Core identity: domain + aggregate root + workflow correlation
message Cover {
string domain = 2;
UUID root = 1;
string correlation_id = 3; // Workflow correlation - flows through all commands/events
}
// Individual event with sequence number and timestamp
message EventPage {
oneof sequence {
uint32 num = 1; // Normal sequenced event
bool force = 2; // Force-write (conflict resolution)
}
google.protobuf.Timestamp created_at = 3;
google.protobuf.Any event = 4;
}
// Point-in-time aggregate state for replay optimization
message Snapshot {
uint32 sequence = 2;
google.protobuf.Any state = 3;
}
// Complete aggregate history
message EventBook {
Cover cover = 1;
Snapshot snapshot = 2;
repeated EventPage pages = 3;
google.protobuf.Any snapshot_state = 5; // business logic sets this
}
Commands follow the same pattern—a CommandBook contains one or more CommandPages targeting a single aggregate:
message CommandPage {
uint32 sequence = 1;
google.protobuf.Any command = 3;
}
message CommandBook {
Cover cover = 1;
repeated CommandPage pages = 2;
SagaCommandOrigin saga_origin = 4; // Tracks origin for compensation flow
}
Schema-First Development
Domain events and commands are defined in your own .proto files. Angzarr's protocol uses google.protobuf.Any to wrap these domain-specific messages, maintaining type information while keeping the framework protocol stable.
// domain/inventory.proto — your domain schema
syntax = "proto3";
package inventory;
// Commands
message CreateProduct {
string sku = 1;
string name = 2;
int32 initial_quantity = 3;
}
message AdjustInventory {
int32 quantity_delta = 1;
string reason = 2;
}
// Events
message ProductCreated {
string sku = 1;
string name = 2;
int32 initial_quantity = 3;
google.protobuf.Timestamp created_at = 4;
}
message InventoryAdjusted {
int32 previous_quantity = 1;
int32 new_quantity = 2;
int32 delta = 3;
string reason = 4;
}
// Snapshot state
message ProductState {
string sku = 1;
string name = 2;
int32 current_quantity = 3;
}
Business logic implements the BusinessLogic service interface. The framework delivers a ContextualCommand—the aggregate's EventBook (snapshot + subsequent events) bundled with the CommandBook—and receives an EventBook containing the resulting events:
// Framework protocol — implemented by your aggregate
message ContextualCommand {
EventBook events = 1; // Current state: snapshot + events since
CommandBook command = 2; // Command(s) to process
}
service BusinessLogic {
rpc Handle(ContextualCommand) returns (EventBook);
}
Your aggregate implementation unpacks the Any-wrapped messages, applies domain logic, and returns events:
// Example: Go aggregate implementation
func (s *ProductAggregate) Handle(ctx context.Context, cmd *angzarr.ContextualCommand) (*angzarr.EventBook, error) {
// Hydrate state from snapshot + events
state := s.rehydrate(cmd.Events)
// Process each command
var newPages []*angzarr.EventPage
for _, page := range cmd.Command.Pages {
events, err := s.processCommand(state, page.Command)
if err != nil {
return nil, err
}
newPages = append(newPages, events...)
}
return &angzarr.EventBook{
Cover: cmd.Events.Cover,
Pages: newPages,
}, nil
}
func (s *ProductAggregate) processCommand(state *ProductState, cmd *anypb.Any) ([]*angzarr.EventPage, error) {
switch cmd.TypeUrl {
case "type.googleapis.com/inventory.CreateProduct":
var create inventory.CreateProduct
if err := cmd.UnmarshalTo(&create); err != nil {
return nil, err
}
// Pure domain logic — no I/O, no framework coupling
if state != nil {
return nil, errors.New("product already exists")
}
return []*angzarr.EventPage, nil
case "type.googleapis.com/inventory.AdjustInventory":
// ... handle adjustment
}
return nil, errors.New("unknown command type")
}
Coordinator Pattern
Angzarr uses coordinators to route messages between external clients and your business logic services. This separation keeps your domain code focused on business rules 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 paths
service BusinessCoordinator {
// Route commands to appropriate BusinessLogic, persist resulting events
rpc Handle(CommandBook) returns (CommandResponse);
// Record events directly (for integration/migration scenarios)
rpc Record(EventBook) returns (CommandResponse);
}
message CommandResponse {
EventBook events = 1; // Events from the command
repeated Projection projections = 2; // Sync projector results (full cascade)
}
The CommandResponse enables request-response patterns where callers need immediate confirmation of state changes and resulting projections—useful for APIs that must return updated read models.
Projections
Unlike frameworks that treat projections as an afterthought, Angzarr provides first-class projection infrastructure. Projectors consume EventBooks and produce typed Projection messages:
message Projection {
Cover cover = 1; // Source aggregate
string projector = 2; // Projector identifier
uint32 sequence = 3; // Last processed event sequence
google.protobuf.Any projection = 4; // Projected read model
}
service Projector {
rpc Handle(EventBook) returns (google.protobuf.Empty); // Async
rpc HandleSync(EventBook) returns (Projection); // Sync
}
service ProjectorCoordinator {
rpc Handle(EventBook) returns (google.protobuf.Empty); // Fan-out async
rpc HandleSync(EventBook) returns (Projection); // Sync with response
}
Synchronous projections enable CQRS patterns where commands must return updated read models—the coordinator orchestrates the command → event → projection pipeline and returns results atomically.
Saga Coordination
Long-running business processes span multiple aggregates and external systems. Angzarr's saga coordinator manages state and compensation without requiring your saga logic to handle persistence or delivery guarantees.
Sagas receive EventBooks (triggering events from any aggregate), execute business logic, and emit commands to other aggregates:
message SagaResponse {
repeated CommandBook commands = 1; // Commands to execute on other aggregates
}
service Saga {
rpc Handle(EventBook) returns (google.protobuf.Empty); // Async
rpc HandleSync(EventBook) returns (SagaResponse); // Sync
}
service SagaCoordinator {
rpc Handle(EventBook) returns (google.protobuf.Empty); // Route to sagas
rpc HandleSync(EventBook) returns (SagaResponse); // Sync pipeline
}
When synchronous, the SagaResponse returns commands that the framework executes before returning to the caller—enabling complex orchestrations to complete within a single request-response cycle.
Synchronous Operation Patterns
Angzarr provides two distinct mechanisms for getting results back to callers. Choose based on your consistency and observability needs.
Sync Mode
Synchronous processing is controlled via the SyncMode enum on commands and events:
enum SyncMode {
SYNC_MODE_NONE = 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)
}
message SyncCommandBook {
CommandBook command = 1;
SyncMode sync_mode = 2;
}
| 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) | Full transactional consistency across aggregates |
The Cascade Flow (SYNC_MODE_CASCADE)
When sync_mode = CASCADE, the framework orchestrates the full cascade before returning:
The key insight: saga-returned commands are executed recursively through the same sync path, and projectors see the entire cascade—not just the initial command's events.
Warning: SYNC_MODE_CASCADE is expensive and should be avoided when possible. Each step adds latency: aggregate hydration, business logic execution, event persistence, saga evaluation, and projector updates—multiplied by every aggregate touched. A saga fanning out to ten aggregates takes roughly ten times longer than a single-aggregate command. SYNC_MODE_NONE with eventual consistency is the better default.
That said, cascade mode exists because it will be necessary at times. Some workflows genuinely require atomic consistency guarantees before returning to the caller. When you need it, you need it—just understand the cost.
SYNC_MODE_SIMPLE: Read-After-Write Without Cascade
For most read-after-write scenarios, SYNC_MODE_SIMPLE is sufficient—projectors run synchronously for the immediate command's events, but sagas fire asynchronously:
Use when:
- REST/GraphQL APIs need to return the updated state for this aggregate
- UI requires immediate feedback after user action
- Saga effects can be eventually consistent
Trade-off: Sagas run asynchronously—the caller won't see saga-triggered events in the response. If a saga fails, compensation happens out-of-band.
Gateway Streaming: Observing Effects in Real-Time
When you need to observe events as they happen rather than waiting for completion, use the gateway's streaming interface:
service CommandGateway {
// Send command, stream events as they occur
rpc ExecuteStream(CommandBook) returns (stream EventBook) {}
// Stream until N events received
rpc ExecuteStreamResponseCount(ExecuteStreamCountRequest) returns (stream EventBook) {}
// Stream for specified duration
rpc ExecuteStreamResponseTime(ExecuteStreamTimeRequest) returns (stream EventBook) {}
}
Events are correlated via correlation_id on Cover, which is shared by both CommandBook and EventBook. This allows clients to track causally-related events across aggregate boundaries.
Use when:
- Building reactive UIs that update progressively as events cascade
- Debugging or tracing command effects through the system
- Long-running workflows where you want incremental feedback
- Fire-and-observe patterns where the client doesn't need to block
Trade-off: Client must handle streaming; must decide when "done" (count, timeout, or explicit signal).
Choosing the Right Mode
| Requirement | NONE | SIMPLE | CASCADE | Gateway Stream |
|---|---|---|---|---|
| Return updated read model | No | Yes (this aggregate) | Yes (full cascade) | No (raw events) |
| See saga effects | No | No (async) | Yes (sync) | Yes (as they occur) |
| Latency | Minimal | + projectors | + full cascade | Minimal |
| Connection model | Request-response | Request-response | Request-response | Long-lived stream |
Recommendation: Start with SYNC_MODE_NONE (eventual consistency). Move to SIMPLE when you need read-after-write. Reserve CASCADE for workflows that genuinely require atomic cross-aggregate consistency. Use gateway streaming for debugging or reactive UIs.
Event Queries
The EventQuery service provides direct access to the event store for replay, debugging, and custom projection rebuilding:
service EventQuery {
// Retrieve events for a specific aggregate
rpc GetEvents(Query) returns (stream EventBook);
// Subscribe to events (live tail with optional historical replay)
rpc Synchronize(stream Query) returns (stream EventBook);
// List all aggregate roots (for full replay scenarios)
rpc GetAggregateRoots(google.protobuf.Empty) returns (stream AggregateRoot);
}
message Query {
Cover cover = 1; // Query by root, correlation_id, or both
oneof selection {
SequenceRange range = 3; // Partial replay
SequenceSet sequences = 4; // Specific sequences
TemporalQuery temporal = 5; // Point-in-time (as_of_time or as_of_sequence)
}
}
message AggregateRoot {
string domain = 1;
UUID root = 2;
}
Streaming responses handle large event histories efficiently. The Synchronize RPC enables catch-up subscriptions—replay historical events then seamlessly transition to live updates.
Pluggable Infrastructure
Angzarr's core abstracts storage and messaging behind adapter interfaces. Custom adapters implement a defined trait.
Event Store Adapters
- SQLite (tested — local development, standalone mode)
- MongoDB (tested — production)
- PostgreSQL (implemented, untested)
- Redis (implemented, untested)
Message Bus Adapters
- Direct gRPC (tested — development, simple deployments)
- RabbitMQ (tested — production)
- Kafka (implemented, untested)
Configuration is declarative:
# config.yaml (production)
storage:
type: mongodb
path: mongodb://user:pass@mongo:27017
database: events
bus:
type: amqp
url: amqp://user:pass@rabbitmq:5672
business_logic:
- domain: inventory
address: localhost:50051
projectors:
- name: inventory-summary
address: localhost:50052
synchronous: true
sagas:
- name: order-fulfillment
address: localhost:50053
# config.yaml (local development)
storage:
type: sqlite
path: ./data/events.db
bus:
type: direct # gRPC calls, no message broker
business_logic:
- domain: inventory
address: localhost:50051
Sidecar Deployment Model
Angzarr runs as a sidecar container alongside your business logic. Each pod contains your service and an Angzarr instance communicating over localhost gRPC.
Security posture:
- Minimal attack surface: ~8MB distroless container with no shell, no package manager, no unnecessary binaries
- No network exposure: Sidecar communicates with your service over localhost only; external traffic routes through your service's existing ingress
- Principle of least privilege: Sidecar requires only outbound connections to event store and message bus
Operational characteristics:
- Horizontal scaling follows your service scaling—no separate capacity planning
- Sidecar failure restarts independently; Kubernetes health checks apply normally
- No shared state between sidecars; coordination happens through event store and bus
- Local gRPC eliminates network latency between your logic and the framework
# Deployment with Angzarr sidecar
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-aggregate
spec:
template:
spec:
containers:
- name: aggregate
image: your-registry/inventory-aggregate:v1
ports:
- containerPort: 50051
- name: angzarr
image: ghcr.io/angzarr-io/angzarr:latest # ~8MB
env:
- name: ANGZARR_SERVICE_ENDPOINT
value: "localhost:50051"
- name: ANGZARR_CONFIG
value: "/etc/angzarr/config.yaml"
volumeMounts:
- name: config
mountPath: /etc/angzarr
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
For local development, use Kind (Kubernetes in Docker) with SQLite storage:
# Local dev with Kind cluster
just kind-create
just deploy
Observability
Implementing teams get OpenTelemetry instrumentation for free. The sidecar/coordinator layer instruments every pipeline—aggregate command handling, saga orchestration, process manager workflows, and projector event processing—so business logic code requires zero observability boilerplate.
What You Get Without Writing Any Instrumentation Code
Every command, saga, and projector execution is traced and metered at the coordinator level. The granularity is the Handle and Prepare boundaries—the exact points where the framework calls into your business logic:
| Pipeline | Traced Spans | Metrics |
|---|---|---|
| Aggregate | aggregate.handle, aggregate.execute, aggregate.load_events, aggregate.persist, aggregate.post_persist, aggregate.sync_projectors | angzarr.command.duration, angzarr.command.total |
| Saga | saga.orchestrate, saga.retry, orchestration.fetch, orchestration.execute | angzarr.saga.duration, angzarr.saga.retry.total, angzarr.saga.compensation.total |
| Process Manager | pm.orchestrate, orchestration.fetch, orchestration.execute | angzarr.pm.duration |
| Projector | projector.handle | angzarr.projector.duration |
| Event Bus | — | angzarr.bus.publish.duration, angzarr.bus.publish.total |
| Storage | — | angzarr.storage.duration_seconds, angzarr.events.stored_total, angzarr.events.loaded_total, angzarr.snapshots.stored_total, angzarr.positions.updated_total |
Every span carries the correlation_id as a field, so distributed traces follow a command through aggregate execution, saga fan-out, and downstream projections without any manual context propagation.
How It Works
Angzarr layers observability at three levels:
1. Structured tracing (always on). Every orchestration function is annotated with #[tracing::instrument]. Spans are named by pipeline phase (aggregate.execute, saga.orchestrate, etc.) and carry domain, root, and correlation_id as structured fields. These work with any tracing subscriber—console output, JSON, or OpenTelemetry.
2. Storage metrics (always on). An aspect-oriented Instrumented wrapper decorates all storage implementations (event store, snapshot store, position store) with counters and latency histograms. This is applied at service composition time, not inside implementations—storage code stays clean:
// Framework applies this at startup — your code never sees it
let store = SqliteEventStore::new(pool);
let store = Instrumented::new(store, "sqlite");
3. OpenTelemetry export (opt-in via otel feature). When built with --features otel, the sidecar exports all three telemetry signals via OTLP:
- Traces — Every pipeline span (
aggregate.execute,saga.orchestrate, etc.) is exported as OTLP spans. W3C TraceContext propagation from inbound gRPC headers means traces from your client through the sidecar to your business logic appear as a single distributed trace. - Metrics — Command duration, saga duration, bus publish latency, storage operation counters, and all other instruments are exported as OTLP metrics with domain and outcome labels.
- Logs — Structured tracing events are exported as OTLP logs, correlated with traces via trace/span IDs.
All three signals flow through a single OTLP endpoint (gRPC or HTTP), making the observability backend a deployment choice rather than a code change.
Configuration follows standard OTel environment variables:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4317"
- name: OTEL_SERVICE_NAME
value: "inventory-aggregate"
- name: ANGZARR_LOG
value: "angzarr=info"
Kubernetes: Grafana Out of the Box
On Kubernetes, the included observability stack deploys alongside your application via Helm:
- OpenTelemetry Collector — Receives OTLP from all sidecars, routes to backends
- Tempo — Distributed trace storage
- Prometheus — Metrics storage (via Collector remote write)
- Loki — Log aggregation
- Grafana — Pre-configured dashboards for command pipeline, saga execution, and projector throughput
Grafana is available immediately after deployment with no additional setup. Dashboards visualize the full command lifecycle across domains.
Cloud Provider Integration
OTLP is the vendor-neutral telemetry protocol. The same sidecar binaries feed any OTLP-compatible backend by changing the collector endpoint:
| Provider | Traces | Metrics | Logs |
|---|---|---|---|
| GCP | Cloud Trace | Cloud Monitoring | Cloud Logging |
| AWS | X-Ray (via ADOT Collector) | CloudWatch Metrics | CloudWatch Logs |
| Self-hosted | Tempo / Jaeger | Prometheus | Loki |
| SaaS | Datadog / Honeycomb / Lightstep | Datadog / Grafana Cloud | Datadog / Grafana Cloud |
No code changes or recompilation required — swap the OTEL_EXPORTER_OTLP_ENDPOINT to point at the appropriate collector.
Correlation ID Flow
The correlation_id on Cover is the thread connecting all operations in a workflow. When a client sends a command, the sidecar generates or preserves the correlation ID and propagates it through every subsequent operation:
In a log aggregation system, filtering by correlation_id=abc-123 shows the entire workflow across all aggregates, sagas, and projectors—without the implementing team adding a single log line.
Why Rust
The framework core is implemented in Rust. This choice is pragmatic, not ideological:
- Memory safety without runtime cost: No garbage collection pauses affecting tail latencies. No null pointer exceptions in production. Memory safety guarantees reduce the class of security vulnerabilities possible in the framework itself.
- Minimal deployment footprint: Sidecar container images are ~8MB. No runtime dependencies, no JVM, no interpreter. Distroless base images with only the Angzarr binary reduce attack surface to the minimum possible.
- Predictable performance: Latency-sensitive paths (command routing, event serialization) benefit from zero-cost abstractions and control over allocation.
- Strong ecosystem for the domain:
tonic(gRPC),prost(protobuf),sqlx/mongodb/bigtabledrivers, andtokio(async runtime) are mature and actively maintained.
Business logic runs in whatever language suits the domain and team. Rust proficiency is not required to use Angzarr. Domain code may import Angzarr client libraries to simplify development, but this is not required — the only contract is gRPC + protobuf.
Deployment Options
Adapting Angzarr to your infrastructure requires a one-time DevOps effort:
| Environment | Effort |
|---|---|
| AWS/GCP managed services | Configuration only |
| Existing Kubernetes cluster | Helm install to namespace |
| Custom infrastructure | Integrate storage/messaging backends |
Once deployed, business logic development requires zero infrastructure knowledge.
Trade-offs and Limitations
Angzarr optimizes for a specific architectural style. It is not universally applicable.
| Fits Well | Consider Alternatives |
|---|---|
| Complex domains needing audit trails | Simple CRUD apps |
| Teams using any gRPC-supported language | Browser-only (no gRPC) |
| Independent command/query scaling | Library-style preference |
| Infrastructure portability | Managed service preference |
Current limitations:
- Multi-tenancy patterns are possible but not first-class; tenant isolation is an application concern
- Multi-region event replication is roadmapped but not yet available (see Roadmap)
Comparison to Alternatives
For detailed comparison against AWS Lambda + Step Functions, GCP Cloud Run, Axon Framework, and Kafka, see COMPARISON.md.
Next Steps
- Getting Started — Prerequisites, installation, and your first domain
- Architecture — Core concepts and data flow
- Components — Aggregates, sagas, projectors, process managers
License
AGPL-3.0 (GNU Affero General Public License v3). See LICENSE for details.
Angzarr is under active development. API stability is not guaranteed before 1.0. Production deployment is not recommended at this time.