Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 14

    Aggregates

    Aggregates cluster entities and value objects with one root that enforces invariants transactionally — the consistency boundary for writes in DDD.

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Aggregates cluster entities and value objects with one root that enforces invariants transactionally — the consistency boundary for writes in DDD. Airbnb booking aggregates protect availability, pricing, and cancellation rules atomically without locking half the listings database.

    Real production story

    Airbnb introduced instant book without a clear aggregate boundary — listing availability, pricing rules, and reservation state updated in three services without a single invariant owner. Double-bookings spiked during high-demand events: two guests confirmed for one night because availability decrements raced. The fix redesigned around a ListingReservation aggregate root: all mutations through BookListing command; optimistic concurrency on version; domain events emitted after commit. Double-bookings dropped to statistical noise; support refunds from overlap fell 95%.

    Business problem

    Airbnb marketplace invariants span listing, calendar, price, and guest rules. Violating aggregate boundaries creates guest trust incidents and host payout disputes — expensive to unwind.

    • Trust: Double-booking destroys host and guest confidence — social media amplifies damage.
    • Financial liability: Wrong cancellation policy application triggers refund classes and chargebacks.
    • Scale: Fine-grained locking on listing row does not scale globally — need correct boundary size.

    Architecture overview

    Aggregate root is the only entry point for mutations. External objects hold references to root ID, not internal entities. Size aggregates for transactional consistency needs — small enough to perform, large enough for invariants.

    • Design rules: One transaction = one aggregate; invariant enforcement inside root.
    • Size heuristic: If two entities always change together, same aggregate; if independently, split.
    • Concurrency: Optimistic versioning on root; reject stale commands with conflict.
    • Events: Raise domain events from aggregate after invariants satisfied — before persistence.

    Architecture motivation

    Aggregates define transaction boundaries. One aggregate instance modified per transaction; references to other aggregates by ID only; eventual consistency across aggregates via events.

    • Force: Concurrent booking attempts on popular listings during events.
    • Constraint: Cannot distributed-lock entire listing catalog — partition by aggregate ID.
    • Outcome: Invariants enforced in domain layer; infrastructure is persistence detail.

    Internal architecture

    Airbnb ListingReservation aggregate — root owns calendar slots and pricing snapshot:

    • Listing details fetched for read — not mutated through Reservation aggregate.
    • Cross-aggregate: Payment aggregate referenced by PaymentId only.
    text
    ListingReservation (aggregate root)
    ├── ReservationId
    ├── ListingId (ref — not embedded Listing entity)
    ├── GuestId (ref)
    ├── DateRange (value object)
    ├── NightlyPriceSnapshot (value object — frozen at book)
    ├── Status (enum: Pending · Confirmed · Cancelled)
    └── CalendarHold (entity — internal, not referenced externally)
    Rules enforced in root:
    · no overlapping confirmed holds
    · cancellation policy applied atomically
    · instant-book vs request-to-book invariants

    Data flow

    BookListing command flow: load aggregate by ListingId+dates key → check invariants → apply state change → persist with version check → publish ReservationConfirmed event → payment saga listens asynchronously.

    • Write: Single DB transaction on aggregate row + calendar hold rows.
    • Read: Query side may denormalize availability — rebuilt from events.
    • Conflict: Version mismatch → 409 to client → user refreshes availability.

    System design diagram

    Two diagrams show the Aggregates topology and the primary request/event path used in production at scale.

    Aggregates — system view
    Book command
    Edge
    Reservation AR
    Core
    Listing calendar
    Data
    Event bus
    Async
    High-level topology for Aggregates.
    Aggregates — request / event flow
    Command
    Ingress
    Load AR
    Store
    Apply rule
    Store
    Persist + event
    Emit
    Follow this path when reviewing production designs.

    Production code example

    ListingReservation aggregate — TypeScript domain model with invariant enforcement:

    • Factory methods encode commands — no public setters on aggregate.
    • Version passed to repo.save enables optimistic concurrency on hot listings.
    typescript
    export class ListingReservation {
    private constructor(
    readonly id: ReservationId,
    readonly listingId: ListingId,
    private guestId: GuestId,
    private range: DateRange,
    private status: ReservationStatus,
    private version: number,
    private holds: CalendarHold[]
    ) {}
    static book(cmd: BookListingCommand): DomainEvent[] {
    const agg = repo.load(cmd.listingId, cmd.range);
    if (agg.holdsOverlap(cmd.range)) {
    throw new AvailabilityConflictError(cmd.listingId, cmd.range);
    }
    agg.applyHold(cmd.guestId, cmd.range, cmd.priceSnapshot);
    agg.status = ReservationStatus.Confirmed;
    agg.version += 1;
    const events = [new ReservationConfirmed(agg.id, agg.listingId, cmd.guestId)];
    repo.save(agg, agg.version - 1); // optimistic check
    return events;
    }
    cancel(policy: CancellationPolicy, actor: GuestId): DomainEvent[] {
    if (this.guestId.value !== actor.value) throw new UnauthorizedCancel();
    const refund = policy.computeRefund(this.range, this.priceSnapshot);
    this.status = ReservationStatus.Cancelled;
    this.version += 1;
    return [new ReservationCancelled(this.id, refund)];
    }
    }

    Enterprise case study

    Airbnb instant book aggregate redesign: Distributed updates caused double-booking during peak demand.

    • Before: Three services update calendar; 0.3% double-book rate on hot listings.
    • Decision: ListingReservation aggregate, optimistic locking, outbox events, saga for payment.
    • After: Double-book near zero; book path p99 120ms; support refund class down 95%.

    Trade-offs

    • Large vs small aggregate: Large = bigger lock scope; small = more eventual consistency complexity.
    • Sync vs async to payment: Confirm booking before payment risks unpaid holds — timeout saga releases hold.
    • Global vs per-listing: One aggregate per listing-night vs per reservation — choose by contention pattern.
    • ORM temptation: Lazy-load entire listing graph — breaks boundary; explicit load root only.

    Security considerations

    Aggregate commands authorize at root: Guest cannot cancel another guest's reservation — check GuestId in root method.

    • Command validation: All inputs validated before state change — no anemic setters.
    • Audit: Domain events capture who/when for dispute resolution.
    • Rate limit: Book command rate limited per guest — anti-scraping at application layer.

    Scalability analysis

    Aggregate design affects hot listing scale: Beyoncé-adjacent property gets thousands of concurrent books — root must be narrow and fast.

    • Hot aggregate: Shard by listingId; short transactions; no external calls inside transaction.
    • Read scaling: CQRS projection for search availability — not aggregate query for browse.
    • Event volume: ReservationConfirmed fan-out async — do not block commit on notifications.

    Failure scenarios

    Aggregate boundary failures: Dual aggregate update in one request; external HTTP inside transaction; missing version check.

    • Double-book: Two roots for same listing — merge to one root per bookable window.
    • Orphan hold: Crash after persist before event — outbox ensures downstream release.
    • Stale read book: User books from stale cache — version conflict returns clear retry UX.

    Staff engineer insights

    • If your transaction touches two aggregate roots, your aggregate boundaries are wrong — redesign before adding distributed transactions.
    • Aggregates should fit in one engineer's head — more than ~3 internal entities warrants workshop scrutiny.
    • External calls inside aggregate methods are the top cause of slow roots and deadlocks — raise events instead.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionHow do you choose aggregate boundaries for an e-commerce order?+

    Answer

    Order aggregate owns line items, status, and totals — invariants on price and quantity together. Product catalog is separate aggregate referenced by SKU. Payment is separate — coordinate via domain events and saga. Shipment might be separate aggregate if fulfillment team owns lifecycle independently.

    Follow-up

    Should Customer be inside Order aggregate?
    2AdvancedQuestionTwo aggregates must stay consistent immediately. Options?+

    Answer

    Redesign to one aggregate if true invariant. If impossible, saga with compensations and accept eventual consistency with business-defined max lag. Avoid 2PC across services. Never dual-update two roots in one transaction across DBs.

    Follow-up

    Example saga for booking + payment?
    3AdvancedQuestionAggregate is too contended at scale. What now?+

    Answer

    Narrow aggregate scope — split by sharding key; move read models to CQRS; queue commands per aggregate ID; consider actor model single-thread per root. Last resort: CRDT or lock-free domain rules if business allows — rare for money/booking.

    Follow-up

    When is large aggregate correct?

    Architecture review questions

    • Is there exactly one aggregate root per consistency cluster?
    • Are cross-aggregate references by ID only?
    • Are invariants enforced inside root methods — not in controllers?
    • Is one aggregate modified per transaction?
    • Is optimistic or pessimistic concurrency defined for hot roots?
    • Are domain events raised from aggregate after invariants pass?

    Summary

    Aggregates at Airbnb protect booking invariants atomically through a ListingReservation root with optimistic concurrency and domain events — eliminating double-bookings that distributed partial updates caused.

    Ready to mark this lesson complete?Track your journey across the entire course.