The Small Footprint Principle
Your aggregate handler is fifty lines. Your saga is twenty. The framework handles the other ten thousand.
The Problem
Section titled “The Problem”Event sourcing implementations typically require thousands of lines of infrastructure code: event persistence, snapshot management, optimistic concurrency, message bus integration, retry logic, dead letter handling. Teams spend months building plumbing before writing a single business rule.
And then they maintain it forever.
The Separation
Section titled “The Separation”Angzarr draws a surgical line between your code and framework code:
| You Write | Framework Handles |
|---|---|
| Command validation | Event persistence |
| Business rules | Optimistic concurrency |
| Event construction | Snapshot management |
| State reconstruction | Message bus integration |
| Retry and backoff | |
| Dead letter queues | |
| Schema evolution | |
| Cross-domain routing |
Your aggregate focuses on one job: given state and command, decide the outcome and produce events. No message bus configuration. No retry logic. No infrastructure concerns leaking into business decisions.
Aggregates may query external systems when they need additional context to decide on a command—checking inventory availability, validating with a payment provider, or fetching current exchange rates. This is one of the integration points where your DDD/CQRS-ES system connects with non-DDD systems, legacy applications, or third-party services. But the core responsibility remains: your aggregate decides the disposition of commands within its domain.
Note that aggregates should query external systems, not mutate them. Side effects to external systems belong in projectors, which react to committed events.
Why Small Matters
Section titled “Why Small Matters”AI Assistance
Section titled “AI Assistance”Modern AI assistants have context windows. A 50-line aggregate fits comfortably. A 5,000-line framework integration doesn’t.
When your business logic is small and isolated, AI can:
- Review entire handlers in one pass
- Suggest improvements with full context
- Generate tests that exercise real behavior
- Refactor without breaking infrastructure
Team Onboarding
Section titled “Team Onboarding”New team members read your domain in an afternoon, not a month. The learning curve is business rules, not framework internals.
Code Review
Section titled “Code Review”Reviews focus on business logic correctness, not infrastructure bugs. When the aggregate is 50 lines, reviewers catch edge cases. When it’s 5,000 lines, they skim.
The Pattern
Section titled “The Pattern”Every aggregate is a class with @handles methods. The base class handles persistence, replay, and dispatch.
class Player(CommandHandler[PlayerState]): domain = "player"
@handles(DepositFunds) def handle_deposit_funds(self, cmd: DepositFunds) -> FundsDeposited: if not self.state.exists: raise CommandRejectedError("Player does not exist") if cmd.amount <= 0: raise CommandRejectedError("Amount must be positive") return FundsDeposited( amount=cmd.amount, new_balance=self.state.bankroll + cmd.amount, )That’s it. No persistence code. No retry logic. No message publishing.
Line Count Comparison
Section titled “Line Count Comparison”A realistic aggregate implementation:
| Approach | Lines of Code |
|---|---|
| Raw implementation (no framework) | 2,000-5,000 |
| Typical ES framework | 500-1,000 |
| Angzarr | 50-200 |
The difference compounds across a system with dozens of aggregates.
See It In Action
Section titled “See It In Action”The poker example demonstrates this principle across six languages. Each aggregate handler is small enough to read in minutes.