Skip to main content

Saga Examples

Cross-domain coordination examples from the poker domain.


Saga: Table → Hand

When a table starts a hand, the table-hand saga translates the table domain's HandStarted event into a hand domain DealCards command.

⍼ Angzarr supports two saga implementation styles:

StyleDescriptionBest For
OO (Object-Oriented)Saga class with @prepares/@reacts_to decoratorsRich translation logic, type safety
FunctionalEventRouter with function handlersSimple mappings, composition
LanguageOOFunctional
Python
Java
C#
Rust
Go
C++

examples/python/table/saga-hand-oo/main.py

class TableHandSaga(Saga):
"""Saga that translates HandStarted events to DealCards commands.

Uses the OO pattern with @prepares and @reacts_to decorators.
"""

name = "saga-table-hand"
input_domain = "table"
output_domain = "hand"

@prepares(table.HandStarted)
def prepare_hand_started(self, event: table.HandStarted) -> list[types.Cover]:
"""Declare the hand aggregate as destination."""
return [
types.Cover(
domain="hand",
root=types.UUID(value=event.hand_root),
)
]

@reacts_to(table.HandStarted)
def handle_hand_started(
self,
event: table.HandStarted,
destinations: list[types.EventBook],
) -> types.CommandBook:
"""Translate HandStarted -> DealCards."""
# Get next sequence from destination state
dest_seq = next_sequence(destinations[0]) if destinations else 0

# Convert SeatSnapshot to PlayerInHand
players = [
hand.PlayerInHand(
player_root=seat.player_root,
position=seat.position,
stack=seat.stack,
)
for seat in event.active_players
]

# Build DealCards command
deal_cards = hand.DealCards(
table_root=event.hand_root,
hand_number=event.hand_number,
game_variant=event.game_variant,
dealer_position=event.dealer_position,
small_blind=event.small_blind,
big_blind=event.big_blind,
)
deal_cards.players.extend(players)

# Return pre-packed CommandBook for full control
from google.protobuf.any_pb2 import Any

cmd_any = Any()
cmd_any.Pack(deal_cards, type_url_prefix="type.googleapis.com/")

return types.CommandBook(
cover=types.Cover(
domain="hand",
root=types.UUID(value=event.hand_root),
),
pages=[
types.CommandPage(
sequence=dest_seq,
command=cmd_any,
)
],
)

Saga: Compensation

When a saga-issued command is rejected, the source aggregate receives a Notification and must emit compensation events. Use @rejected decorators to register handlers — no if/else chains needed.

examples/python/player/agg/handlers/player.py


@rejected(domain="table", command="JoinTable")
def handle_join_rejected(
self, notification: types.Notification
) -> player_proto.FundsReleased:
"""Release reserved funds when table join fails.

Called when the JoinTable command (issued by saga-player-table after
FundsReserved) is rejected by the Table aggregate.
"""
ctx = CompensationContext.from_notification(notification)

logger.warning(
"Player compensation for JoinTable rejection: reason=%s",
ctx.rejection_reason,
)

# Extract table_root from the rejected command
table_root = b""
if ctx.rejected_command and ctx.rejected_command.cover:
table_root = ctx.rejected_command.cover.root.value

# Release the funds that were reserved for this table
reserved_amount = self.table_reservations.get(table_root.hex(), 0)
new_reserved = self.reserved_funds - reserved_amount
new_available = self.bankroll - new_reserved

return player_proto.FundsReleased(
amount=poker_types.Currency(amount=reserved_amount, currency_code="CHIPS"),
table_root=table_root,
new_available_balance=poker_types.Currency(
amount=new_available, currency_code="CHIPS"
),
new_reserved_balance=poker_types.Currency(
amount=new_reserved, currency_code="CHIPS"
),
released_at=now(),
)


Saga Principles

  1. Stateless — Each event processed independently
  2. Minimal logic — Just translate fields between domains
  3. Single source — Events from one domain only
  4. Single target — Commands to one domain only
  5. Use destination state — Set sequence from destination's next_sequence()

Running Sagas

OO Pattern (Java/Python)

OO sagas use annotation-based routing. The framework discovers @Prepares and @ReactsTo methods via reflection:

// Main entry point
public static void main(String[] args) {
SagaHandler handler = new SagaHandler(new TableHandSaga());
SagaServer.run("saga-table-hand", "50411", handler);
}

Functional Pattern (C#/Go/Rust/C++)

Functional sagas use explicit router registration:

public static EventRouter Create()
{
return new EventRouter("saga-table-hand")
.Domain("table")
.Prepare<HandStarted>(PrepareHandStarted)
.On<HandStarted>(HandleHandStarted);
}

Next Steps