Skip to main content

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:

LanguageLOCImplementation
Go173examples/go/customer/logic/
Python209examples/python/customer/
Rust303examples/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:

DomainScenariosFeature File
Cart27cart.feature
Customer15customer.feature
Fulfillment19fulfillment.feature
Inventory19inventory.feature
Order22order.feature
Product18product.feature
Saga Cancellation5saga_cancellation.feature
Saga Fulfillment4saga_fulfillment.feature
Saga Loyalty Earn4saga_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 DefineYou ImplementWe Handle
Commands in .protogRPC BusinessLogic serviceEvent persistence
Events in .protogRPC Projector servicesOptimistic concurrency
Read models in .protogRPC Saga servicesSnapshot management
Event upcasting
Event distribution
Saga coordination
Schema evolution rules

What Angzarr Handles (So You Don't)

ConcernHandled By
Database transactionsAngzarr
Optimistic concurrencyAngzarr
Event orderingAngzarr
Retry logicAngzarr
Network failuresAngzarr
Service discoveryAngzarr
Load balancingAngzarr
State hydrationAngzarr
Snapshot managementAngzarr
Event upcastingAngzarr
ObservabilityAngzarr
Message serializationProtobuf
Schema evolutionProtobuf
DeploymentDevOps/Helm
ScalingK8s/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;
}
ModeProjectorsSagasUse Case
NONEAsyncAsyncFire-and-forget, eventual consistency
SIMPLESyncAsyncRead-after-write for single aggregate
CASCADESyncSync (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

RequirementNONESIMPLECASCADEGateway Stream
Return updated read modelNoYes (this aggregate)Yes (full cascade)No (raw events)
See saga effectsNoNo (async)Yes (sync)Yes (as they occur)
LatencyMinimal+ projectors+ full cascadeMinimal
Connection modelRequest-responseRequest-responseRequest-responseLong-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:

PipelineTraced SpansMetrics
Aggregateaggregate.handle, aggregate.execute, aggregate.load_events, aggregate.persist, aggregate.post_persist, aggregate.sync_projectorsangzarr.command.duration, angzarr.command.total
Sagasaga.orchestrate, saga.retry, orchestration.fetch, orchestration.executeangzarr.saga.duration, angzarr.saga.retry.total, angzarr.saga.compensation.total
Process Managerpm.orchestrate, orchestration.fetch, orchestration.executeangzarr.pm.duration
Projectorprojector.handleangzarr.projector.duration
Event Busangzarr.bus.publish.duration, angzarr.bus.publish.total
Storageangzarr.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:

ProviderTracesMetricsLogs
GCPCloud TraceCloud MonitoringCloud Logging
AWSX-Ray (via ADOT Collector)CloudWatch MetricsCloudWatch Logs
Self-hostedTempo / JaegerPrometheusLoki
SaaSDatadog / Honeycomb / LightstepDatadog / Grafana CloudDatadog / 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/bigtable drivers, and tokio (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:

EnvironmentEffort
AWS/GCP managed servicesConfiguration only
Existing Kubernetes clusterHelm install to namespace
Custom infrastructureIntegrate 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 WellConsider Alternatives
Complex domains needing audit trailsSimple CRUD apps
Teams using any gRPC-supported languageBrowser-only (no gRPC)
Independent command/query scalingLibrary-style preference
Infrastructure portabilityManaged 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


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.