Polyglot Architecture
Your Python team writes the ML projector. Your Rust team writes the latency-critical aggregate. Your Java team maintains the legacy integration. Same events. Same behavior.
The Promise
Six languages. One event stream. Identical behavior verified by the same test suite.
| Language | Client Library | Typical Use |
|---|---|---|
| Python | angzarr-client | ML projectors, analytics, scripting |
| Rust | angzarr-client | High-performance aggregates |
| Go | github.com/angzarr/client | Microservices, infrastructure |
| Java | dev.angzarr:client | Enterprise integrations |
| C# | Angzarr.Client | .NET ecosystems |
| C++ | header-only | Embedded, performance-critical |
Any language with gRPC support works. These six have thin client libraries that reduce boilerplate.
Proto as Contract
The magic is Protocol Buffers. Your data model—commands, events, state—lives in .proto files:
// Shared across all languages
message PlayerActed {
string hand_id = 1;
string player_id = 2;
ActionType action = 3;
int64 amount = 4;
}
enum ActionType {
FOLD = 0;
CHECK = 1;
CALL = 2;
RAISE = 3;
ALL_IN = 4;
}
Generate bindings for each language. The types are the contract.
The architecture could use alternative serialization and RPC technologies, but Protocol Buffers and gRPC are fast, well-supported across languages, and already in use in enterprises today.
Same Behavior, Verified
All six implementations share the same Gherkin specifications:
# examples/features/player.feature
Scenario: Player deposits funds
Given a registered player with $100 balance
When they deposit $50
Then their balance is $150
And a FundsDeposited event is recorded
Each language runs these scenarios against its implementation. If the tests pass, behavior is identical.
Example: Poker Platform
- Rust: Hand aggregate handles player actions during live play. Maintainability and security with high performance.
- Python: ML projector consumes events for model training and real-time predictions. PyTorch integration is natural.
- Java: Payment gateway integration speaks to legacy banking APIs. The team already knows the ecosystem.
Each component:
- Receives the same events
- Uses the same proto types
- Deploys independently
- Tests against shared Gherkin specs
Gradual Migration
Start with one language. Add others as needed.
Month 1: Python prototype
└── Prove the architecture
Month 3: Rust for hot path
└── Performance requirements emerge
Month 6: Java for banking integration
└── Existing team, existing code
Month 12: All three in production
└── Each optimized for its role
No big bang. No coordinated rewrites. Components evolve at their own pace.
Team Autonomy
Different teams, different strengths:
| Team | Expertise | Responsibility |
|---|---|---|
| Core platform | Rust | Hand/table aggregates |
| Data science | Python | ML projectors, analytics |
| Integrations | Java | Payment, KYC, legacy APIs |
| Mobile API | Go | Edge services, caching |
Teams choose their tools. The event stream is the integration point.
Client Library Comparison
Each library follows the same patterns:
- Python
- Rust
- Go
- Java
- C#
- C++
@command_handler(player.DepositFunds)
def handle_deposit(
cmd: player.DepositFunds, state: PlayerState, seq: int
) -> player.FundsDeposited:
"""Deposit funds into player's bankroll."""
if not state.exists:
raise CommandRejectedError("Player does not exist")
amount = cmd.amount.amount if cmd.amount else 0
if amount <= 0:
raise CommandRejectedError("amount must be positive")
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(),
)
pub fn handle_deposit_funds(
command_book: &CommandBook,
command_any: &Any,
state: &PlayerState,
seq: u32,
) -> CommandResult<EventBook> {
let cmd: DepositFunds = command_any
.unpack()
.map_err(|e| CommandRejectedError::new(format!("Failed to decode: {}", e)))?;
guard(state)?;
let amount = validate(&cmd)?;
let event = compute(&cmd, state, amount);
let event_any = pack_event(&event, "examples.FundsDeposited");
Ok(new_event_book(command_book, seq, event_any))
}
func HandleDepositFunds(
commandBook *pb.CommandBook,
commandAny *anypb.Any,
state PlayerState,
seq uint32,
) (*pb.EventBook, error) {
var cmd examples.DepositFunds
if err := proto.Unmarshal(commandAny.Value, &cmd); err != nil {
return nil, err
}
if err := guardDepositFunds(state); err != nil {
return nil, err
}
amount, err := validateDepositFunds(&cmd)
if err != nil {
return nil, err
}
event := computeFundsDeposited(&cmd, state, amount)
eventAny, _ := anypb.New(event)
return angzarr.NewEventBook(commandBook.Cover, seq, eventAny), nil
}
public static FundsDeposited handle(DepositFunds cmd, PlayerState state) {
guard(state);
long amount = validate(cmd);
return compute(cmd, state, amount);
}
public static FundsDeposited Handle(DepositFunds cmd, PlayerState state)
{
Guard(state);
var amount = Validate(cmd);
return Compute(cmd, state, amount);
}
examples::FundsDeposited handle_deposit(const examples::DepositFunds& cmd,
const PlayerState& state) {
guard(state);
int64_t amount = validate(cmd);
return compute(cmd, state, amount);
}
Same pattern. Same semantics. Different syntax.
The Framework Doesn't Care
Behind the gRPC endpoint, Angzarr sees:
- A proto request
- A proto response
Whether your handler is Python, Rust, Java, or anything else—the framework doesn't know or care. It sends commands, receives events, manages persistence.
Your code is a black box that speaks proto.
See Also
- SDKs — Language-specific documentation
- Small footprint — Why handlers stay simple
- Getting started — Pick a language and begin