Skip to content

The Small Footprint Principle

Your aggregate handler is fifty lines. Your saga is twenty. The framework handles the other ten thousand.


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.


Angzarr draws a surgical line between your code and framework code:

You WriteFramework Handles
Command validationEvent persistence
Business rulesOptimistic concurrency
Event constructionSnapshot management
State reconstructionMessage 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.


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

New team members read your domain in an afternoon, not a month. The learning curve is business rules, not framework internals.

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.


Every aggregate is a class with @handles methods. The base class handles persistence, replay, and dispatch.

illustrative - handler pattern
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.


A realistic aggregate implementation:

ApproachLines of Code
Raw implementation (no framework)2,000-5,000
Typical ES framework500-1,000
Angzarr50-200

The difference compounds across a system with dozens of aggregates.


The poker example demonstrates this principle across six languages. Each aggregate handler is small enough to read in minutes.

View the Player aggregate →