Introduction to Angzarr
⍼ Angzarr is a polyglot framework for building event-sourced systems. You write business logic in any language with gRPC support—the framework handles event persistence, saga coordination, projection management, and all the infrastructure complexity that typically derails CQRS/ES projects.
The symbol ⍼ (U+237C, "angzarr") has existed in Unicode since 2002 without a 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.
This codebase uses AI-generated code under human supervision. The implementation has been reviewed, but with an emphasis on velocity over thoroughness.
This is not AI slop. The gRPC/protobuf definitions were hand-written and have evolved with heavy oversight. The architecture and code derive from an earlier human-written Go codebase—ported to Rust and used as the starting point—when the author switched for performance, binary size, and cleaner deployment mechanisms. Polyglot support has always been a core design goal—world domination requires meeting developers where they are. Architecturally, it's sound—the framework design reflects years of thinking about CQRS/ES patterns.
Approaching production-ready, but not there yet. The code is stabilizing and mostly working, but plan for additional testing and hardening before production deployment.
Contributors are welcome. There are a few rough edges, but mostly the project needs code review, test coverage expansion, and developers willing to build on it and report what breaks. If you're interested in CQRS/ES tooling and want to be an early adopter, jump in.
What Angzarr Provides
Angzarr inverts the typical framework relationship. Rather than providing libraries that applications import, Angzarr provides infrastructure that applications connect to via gRPC.
Your data model lives in .proto files, not code. Commands, events, and state are defined as Protocol Buffer messages—language-neutral, versionable, and shared across all implementations. This is what enables true polyglot support: the same event stream can be produced by a Rust aggregate and consumed by a Python projector.
| You Define | You Implement | We Handle |
|---|---|---|
Commands in .proto | Aggregate handlers | Event persistence |
Events in .proto | Projector handlers | Optimistic concurrency |
State in .proto | Saga handlers | Snapshot management |
| Event upcasting | ||
| Event distribution | ||
| Saga coordination | ||
| Schema evolution |
Your business logic receives commands with full event history and emits events. No database connections. No message bus configuration. No retry logic. Pure domain logic.
Architecture Preview
⍼ Angzarr stores aggregate history as an EventBook—the complete event stream for a single aggregate root: its identity (the Cover), an optional Snapshot for efficient replay, and ordered EventPages representing domain events.
The dashed Domain B represents any additional domain(s)—sagas bridge events from one domain to commands in another. Real systems have multiple domains, each with its own aggregate.
Each component type runs in its own pod with an ⍼ Angzarr sidecar. Your code handles business logic; the sidecar handles persistence, messaging, and coordination.
Language Support
Any language with gRPC support works. Your business logic communicates with ⍼ Angzarr coordinators via gRPC—the framework doesn't care what's behind the endpoint. If your language appears on the gRPC supported languages matrix, you can use it with Angzarr. This includes C#, C++, Dart, Go, Java, Kotlin, Node.js, Objective-C, PHP, Python, Ruby, Rust, and more.
Client libraries are optional and minimal. For six languages (the top TIOBE languages), we provide thin client libraries that reduce boilerplate—protobuf packing/unpacking, state reconstruction, router registration. These libraries are intentionally kept lightweight; the real contract is just gRPC + protobuf. You can always work directly with the proto bindings if you prefer:
| Language | Client Library | Example |
|---|---|---|
| Python | angzarr-client | examples/python/ |
| Go | github.com/benjaminabbitt/angzarr/client | examples/go/ |
| Rust | angzarr-client | examples/rust/ |
| Java | dev.angzarr:client | examples/java/ |
| C# | Angzarr.Client | examples/csharp/ |
| C++ | header-only | examples/cpp/ |
All six implementations share the same Gherkin specifications, ensuring identical behavior across languages.
Quick Example
Two styles, same behavior: Functional (pure functions) and OO (class-based with decorators).
- Functional
- OO
Free functions following guard → validate → compute. Easy to unit test—call directly with state, assert on output.
- Python
- Rust
- Go
- Java
- C#
- C++
def deposit_guard(state: PlayerState) -> None:
"""Check state preconditions before processing deposit."""
if not state.exists:
raise CommandRejectedError("Player does not exist")
# docs:end:deposit_guard
# docs:start:deposit_validate
def deposit_validate(cmd: player.DepositFunds) -> int:
"""Validate deposit command and extract amount."""
amount = cmd.amount.amount if cmd.amount else 0
if amount <= 0:
raise CommandRejectedError("amount must be positive")
return amount
# docs:end:deposit_validate
# docs:start:deposit_compute
def deposit_compute(
cmd: player.DepositFunds, state: PlayerState, amount: int
) -> player.FundsDeposited:
"""Build FundsDeposited event from validated inputs."""
new_balance = state.bankroll + amount
return player.FundsDeposited(
amount=cmd.amount,
new_balance=poker_types.Currency(amount=new_balance, currency_code="CHIPS"),
deposited_at=now(),
)
fn guard(state: &PlayerState) -> CommandResult<()> {
if !state.exists() {
return Err(CommandRejectedError::new("Player does not exist"));
}
Ok(())
}
// docs:end:deposit_guard
// docs:start:deposit_validate
fn validate(cmd: &DepositFunds) -> CommandResult<i64> {
let amount = cmd.amount.as_ref().map(|c| c.amount).unwrap_or(0);
if amount <= 0 {
return Err(CommandRejectedError::new("amount must be positive"));
}
Ok(amount)
}
// docs:end:deposit_validate
// docs:start:deposit_compute
fn compute(cmd: &DepositFunds, state: &PlayerState, amount: i64) -> FundsDeposited {
let new_balance = state.bankroll + amount;
FundsDeposited {
amount: cmd.amount.clone(),
new_balance: Some(Currency {
amount: new_balance,
currency_code: "CHIPS".to_string(),
}),
deposited_at: Some(angzarr_client::now()),
}
}
func guardDepositFunds(state PlayerState) error {
if !state.Exists() {
return angzarr.NewCommandRejectedError("Player does not exist")
}
return nil
}
// docs:end:deposit_guard
// docs:start:deposit_validate
func validateDepositFunds(cmd *examples.DepositFunds) (int64, error) {
amount := int64(0)
if cmd.Amount != nil {
amount = cmd.Amount.Amount
}
if amount <= 0 {
return 0, angzarr.NewCommandRejectedError("amount must be positive")
}
return amount, nil
}
// docs:end:deposit_validate
// docs:start:deposit_compute
func computeFundsDeposited(cmd *examples.DepositFunds, state PlayerState, amount int64) *examples.FundsDeposited {
newBalance := state.Bankroll + amount
return &examples.FundsDeposited{
Amount: cmd.Amount,
NewBalance: &examples.Currency{Amount: newBalance, CurrencyCode: "CHIPS"},
DepositedAt: timestamppb.New(time.Now()),
}
}
static void guard(PlayerState state) {
if (!state.exists()) {
throw Errors.CommandRejectedError.preconditionFailed("Player does not exist");
}
}
// docs:end:deposit_guard
// docs:start:deposit_validate
static long validate(DepositFunds cmd) {
long amount = cmd.hasAmount() ? cmd.getAmount().getAmount() : 0;
if (amount <= 0) {
throw Errors.CommandRejectedError.invalidArgument("amount must be positive");
}
return amount;
}
// docs:end:deposit_validate
// docs:start:deposit_compute
static FundsDeposited compute(DepositFunds cmd, PlayerState state, long amount) {
long newBalance = state.getBankroll() + amount;
return FundsDeposited.newBuilder()
.setAmount(cmd.getAmount())
.setNewBalance(Currency.newBuilder().setAmount(newBalance).setCurrencyCode("CHIPS"))
.setDepositedAt(now())
.build();
}
internal static void Guard(PlayerState state)
{
if (!state.Exists)
throw CommandRejectedError.PreconditionFailed("Player does not exist");
}
// docs:end:deposit_guard
// docs:start:deposit_validate
internal static long Validate(DepositFunds cmd)
{
var amount = cmd.Amount?.Amount ?? 0;
if (amount <= 0)
throw CommandRejectedError.InvalidArgument("amount must be positive");
return amount;
}
// docs:end:deposit_validate
// docs:start:deposit_compute
internal static FundsDeposited Compute(DepositFunds cmd, PlayerState state, long amount)
{
var newBalance = state.Bankroll + amount;
return new FundsDeposited
{
Amount = cmd.Amount,
NewBalance = new Currency { Amount = newBalance, CurrencyCode = "CHIPS" },
DepositedAt = Timestamp.FromDateTime(DateTime.UtcNow),
};
}
void guard(const PlayerState& state) {
if (!state.exists()) {
throw angzarr::CommandRejectedError::precondition_failed("Player does not exist");
}
}
// docs:end:deposit_guard
// docs:start:deposit_validate
int64_t validate(const examples::DepositFunds& cmd) {
int64_t amount = cmd.has_amount() ? cmd.amount().amount() : 0;
if (amount <= 0) {
throw angzarr::CommandRejectedError::invalid_argument("amount must be positive");
}
return amount;
}
// docs:end:deposit_validate
// docs:start:deposit_compute
examples::FundsDeposited compute(const examples::DepositFunds& cmd, const PlayerState& state,
int64_t amount) {
int64_t new_balance = state.bankroll + amount;
examples::FundsDeposited event;
event.mutable_amount()->CopyFrom(cmd.amount());
event.mutable_new_balance()->set_amount(new_balance);
event.mutable_new_balance()->set_currency_code("CHIPS");
auto now = std::chrono::system_clock::now();
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
event.mutable_deposited_at()->set_seconds(seconds);
return event;
}
Class-based handlers with decorator/annotation registration. State managed by base class.
- Python
- Rust
- Go
- Java
- C#
- C++
@handles(table_proto.CreateTable)
def create(self, cmd: table_proto.CreateTable) -> table_proto.TableCreated:
"""Create a new table."""
if self.exists:
raise CommandRejectedError("Table already exists")
if not cmd.table_name:
raise CommandRejectedError("table_name is required")
if cmd.small_blind <= 0:
raise CommandRejectedError("small_blind must be positive")
if cmd.big_blind <= 0:
raise CommandRejectedError("big_blind must be positive")
if cmd.big_blind < cmd.small_blind:
raise CommandRejectedError("big_blind must be >= small_blind")
if cmd.max_players < 2 or cmd.max_players > 10:
raise CommandRejectedError("max_players must be between 2 and 10")
return table_proto.TableCreated(
table_name=cmd.table_name,
game_variant=cmd.game_variant,
small_blind=cmd.small_blind,
big_blind=cmd.big_blind,
min_buy_in=cmd.min_buy_in or cmd.big_blind * 20,
max_buy_in=cmd.max_buy_in or cmd.big_blind * 100,
max_players=cmd.max_players or 9,
action_timeout_seconds=cmd.action_timeout_seconds or 30,
created_at=now(),
)
#[aggregate(name = "table", domain = "table", state = TableState)]
impl TableAggregate {
#[handles(CreateTable)]
fn handle_create(
&self,
_cmd: &CommandBook,
create: &CreateTable,
state: &TableState,
seq: u32,
) -> Result<EventBook, String> {
// Guard: table must not exist
if !state.table_name.is_empty() {
return Err("Table already exists".into());
}
// Validate: required fields
if create.table_name.is_empty() {
return Err("table_name is required".into());
}
if create.small_blind == 0 {
return Err("small_blind must be positive".into());
}
// Compute: create the event
Ok(EventBook::single_event(
"table",
"TableCreated",
seq,
TableCreated {
table_name: create.table_name.clone(),
small_blind: create.small_blind,
big_blind: create.big_blind,
max_players: create.max_players,
..Default::default()
}
.encode_to_vec(),
))
}
#[applies(TableCreated)]
fn apply_created(state: &mut TableState, event: &TableCreated) {
state.table_name = event.table_name.clone();
state.small_blind = event.small_blind;
state.big_blind = event.big_blind;
state.max_players = event.max_players;
}
}
// NewHand creates a new Hand command handler with prior events for state reconstruction.
func NewHand(eventBook *pb.EventBook) *Hand {
h := &Hand{}
h.Init(eventBook, func() HandState {
return HandState{
Players: make(map[string]*PlayerHandState),
Pots: []*PotState{{PotType: "main"}},
}
})
h.SetDomain("hand")
// Register event appliers
h.Applies(h.applyCardsDealt)
h.Applies(h.applyBlindPosted)
h.Applies(h.applyActionTaken)
h.Applies(h.applyBettingRoundComplete)
h.Applies(h.applyCommunityCardsDealt)
h.Applies(h.applyDrawCompleted)
h.Applies(h.applyShowdownStarted)
h.Applies(h.applyCardsRevealed)
h.Applies(h.applyCardsMucked)
h.Applies(h.applyPotAwarded)
h.Applies(h.applyHandComplete)
// Register command handlers
h.Handles(h.dealCards)
h.Handles(h.postBlind)
h.Handles(h.playerAction)
h.Handles(h.dealCommunityCards)
h.Handles(h.requestDraw)
h.Handles(h.revealCards)
h.HandlesMulti(h.awardPot)
return h
}
@CommandHandler
public FundsDeposited handleDeposit(DepositFunds cmd, PlayerState state) {
guard(state);
long amount = validate(cmd);
return compute(cmd, state, amount);
}
[CommandHandler]
public FundsDeposited HandleDeposit(DepositFunds cmd, PlayerState state)
{
Guard(state);
var amount = Validate(cmd);
return Compute(cmd, state, amount);
}
Player::Player() {
// Register command handlers
register_handler<examples::RegisterPlayer, examples::PlayerRegistered>(
[this](const examples::RegisterPlayer& cmd, const PlayerState&) {
return handle_register(cmd);
});
register_handler<examples::DepositFunds, examples::FundsDeposited>(
[this](const examples::DepositFunds& cmd, const PlayerState&) {
return handle_deposit(cmd);
});
register_handler<examples::WithdrawFunds, examples::FundsWithdrawn>(
[this](const examples::WithdrawFunds& cmd, const PlayerState&) {
return handle_withdraw(cmd);
});
register_handler<examples::ReserveFunds, examples::FundsReserved>(
[this](const examples::ReserveFunds& cmd, const PlayerState&) {
return handle_reserve(cmd);
});
register_handler<examples::ReleaseFunds, examples::FundsReleased>(
[this](const examples::ReleaseFunds& cmd, const PlayerState&) {
return handle_release(cmd);
});
register_handler<examples::TransferFunds, examples::FundsTransferred>(
[this](const examples::TransferFunds& cmd, const PlayerState&) {
return handle_transfer(cmd);
});
}
examples::PlayerRegistered Player::handle_register(const examples::RegisterPlayer& cmd) {
return handlers::handle_register(cmd, state_);
}
examples::FundsDeposited Player::handle_deposit(const examples::DepositFunds& cmd) {
return handlers::handle_deposit(cmd, state_);
}
examples::FundsWithdrawn Player::handle_withdraw(const examples::WithdrawFunds& cmd) {
return handlers::handle_withdraw(cmd, state_);
}
examples::FundsReserved Player::handle_reserve(const examples::ReserveFunds& cmd) {
return handlers::handle_reserve(cmd, state_);
}
examples::FundsReleased Player::handle_release(const examples::ReleaseFunds& cmd) {
return handlers::handle_release(cmd, state_);
}
examples::FundsTransferred Player::handle_transfer(const examples::TransferFunds& cmd) {
return handlers::handle_transfer(cmd, state_);
}
No database code. No message bus code. Just business logic.
For Decision Makers
If you're evaluating Angzarr for your organization:
- Technical Pitch — Complete architectural pitch with detailed rationale
- Architecture — Core concepts: data model, coordinators, sync modes
- Why Poker — Why our example domain exercises every pattern
For Developers
Ready to build:
- Getting Started — Prerequisites, installation, first aggregate
- Components — Aggregates, sagas, projectors, process managers
- Examples — Code samples in all six languages
Next Steps
- Understand the patterns — CQRS & Event Sourcing Explained
- See the architecture — Architecture
- Get hands-on — Getting Started