Bounded Contexts
Bounded contexts delimit a domain model's applicability — inside the boundary, terms and rules are consistent; across boundaries, integrate via translation, not shared entities.
Introduction
Bounded contexts delimit a domain model's applicability — inside the boundary, terms and rules are consistent; across boundaries, integrate via translation, not shared entities. Google Ads spans billing, targeting, and serving — each context owns its model and ubiquitous language.
Real production story
Google Ads engineers shared a "Campaign" entity across serving, billing, and reporting teams. Serving needed microsecond budget checks; billing needed immutable invoice lines; reporting needed historical snapshots. One ORM model served none well — serving skipped validations, billing got mutable state bugs. Bounded context split produced CampaignServing, BillingContract, and ReportingSnapshot with ACL mappers. p99 serve path simplified; billing disputes dropped; reporting backfills became event-driven without touching serve hot path.
Business problem
Google Ads is multiple businesses in one product surface. Without bounded contexts, one entity name hides incompatible invariants and couples release trains.
- Performance isolation: Serving context cannot wait for billing schema migrations.
- Correctness: Financial invariants differ from ad delivery heuristics — one model compromises both.
- Team autonomy: Hundreds of engineers need deploy independence within clear seams.
Architecture overview
Bounded context packages entities, value objects, aggregates, services, and repositories under one consistent model. Boundaries are social and technical: team ownership, API contracts, separate persistence.
- Boundary signals: Terminology forks, different lifecycles, different SLAs on same noun.
- Integration: Domain events, public APIs, ACL — never shared mutable tables.
- Size: Context too large = mini monolith; too small = integration tax — fit team cognitive load.
- Testing: Contract tests at boundary; internal model tests stay private.
Architecture motivation
Bounded context is the unit of model consistency. Staff architects enforce: one aggregate root ownership, one ubiquitous language, explicit integration — not shared database tables.
- Force: Same noun, different rules — "Campaign" is not one thing.
- Constraint: Legacy shared DB — strangler with ACL and dual-write reconciliation.
- Outcome: Engineers reason locally; cross-context changes are explicit integration projects.
Internal architecture
Google Ads bounded contexts — separate models, explicit integration:
- No foreign keys across context databases — only correlation IDs in events.
- ACL in reporting ingests legacy CSV without polluting serving model.
┌──────────────────┐│ Serving Context │ hot path · microsecond budgets│ CampaignServe AR │ eventual budget from Billing events└────────┬─────────┘│ CampaignBudgetUpdated (event)▼┌──────────────────┐│ Billing Context │ strong invariants · invoice lines│ BillingContract │└────────┬── ───────┘│ InvoiceLineCreated (event)▼┌──────────────────┐│ Reporting Context│ append-only snapshots│ CampaignSnapshot │└──────────────────┘
Data flow
Within context: command → aggregate → persist → publish event. Across contexts: consumer translates event to local model via ACL — never import foreign entities.
- Serving write: Impression logged; budget check against local projection.
- Billing sync: Consumes delivery events; emits budget updates serving consumes.
- Reporting: Async snapshot from both; never blocks serve path.
System design diagram
Two diagrams show the Bounded Contexts topology and the primary request/event path used in production at scale.
Production code example
Anti-corruption layer mapper — Java between billing and serving contexts:
- ACL is the only file allowed to know both sides' DTO shapes — enforce with ArchUnit.
- Map to local value objects, not foreign entities.
public final class BillingToServingAcl {public ServingBudgetProjection map(CampaignBudgetUpdated event) {return ServingBudgetProjection.builder().campaignId(CampaignId.of(event.campaignId())).remainingMicros(event.remainingBudgetMicros()).currency(CurrencyCode.of(event.currency())).asOf(Instant.ofEpochMilli(event.occurredAt())).build();}public CampaignServeCommand map(ServeAdRequest request, ServingBudgetProjection budget) {if (budget.remainingMicros() < request.estimatedCostMicros()) {throw new InsufficientBudgetException(request.campaignId());}return CampaignServeCommand.create(request.campaignId(),request.adGroupId(),request.auctionId());}}// Serving context never imports billing.domain.Campaign
Enterprise case study
Google Ads Campaign split: Unified Campaign entity blocked serving performance and billing correctness.
- Before: Shared Postgres schema; serve p99 regressions on billing deploy days.
- Decision: Three bounded contexts, separate stores, event choreography, ACL for legacy reporting.
- After: Serve and billing deploy independently; billing dispute rate down 40%; reporting lag explicit in SLA.
Trade-offs
- Duplication vs coupling: Duplicate Campaign ID and subset of fields — avoid shared entity JAR.
- Eventual consistency: Serving budget projection may lag billing — define max staleness SLA.
- Migration cost: Splitting context from shared DB takes quarters — plan reconciliation.
- Context granularity: Split when change rates diverge; merge when integration cost exceeds benefit.
Security considerations
Context boundary enforces least privilege: Serving service account cannot write billing ledger.
- IAM per context: Separate DB roles, KMS keys, and audit logs.
- PII scope: Billing holds payment instruments; serving holds pseudonymous IDs only.
- Event payload: Scrub PII at publish boundary — consumers trust minimum data.
Scalability analysis
Bounded contexts enable scale: Serving scales horizontally; billing scales on batch windows; reporting scales on pipeline workers — independent.
- Data partition: Each context DB shards by customer ID independently.
- Team scale: New team = new context only if boundary is clear — avoid context sprawl.
- Catalog: Internal developer portal lists contexts, events, and owners.
Failure scenarios
Boundary violations cause incidents: Direct SQL join across contexts; shared cache key namespace collision; deploying billing migration breaks serving.
- Leaky boundary: Serving reads billing table — circuit break when billing slow takes down ads.
- Schema coupling: Shared migration — separate flyway per context database.
- Event schema break: Version events; consumers support n and n-1.
Staff engineer insights
- Bounded context is where your ubiquitous language stops being ambiguous — if two teams argue about one class, you need two contexts.
- Shared database is the anti-pattern that kills bounded contexts — the schema is the hidden coupling layer.
- Context size should match what one team can hold in working memory — not what fits on one poster.
Interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1AdvancedQuestionHow is bounded context different from a microservice?+
Answer
Follow-up
2AdvancedQuestionYou have a shared database blocking context split. Migration plan?+
Answer
Follow-up
3AdvancedQuestionHow big should a bounded context be?+
Answer
Follow-up
Architecture review questions
- Does each context have one team owner and separate persistence?
- Are cross-context references only via IDs in events or APIs?
- Is ACL present where models translate — not direct entity import?
- Are ubiquitous language glossaries per context documented?
- Do contract tests validate published event/API schemas?
- Is shared database usage explicitly zero or on deprecation timeline?
Summary
Bounded contexts at Google scale isolate incompatible models behind explicit boundaries — serving, billing, and reporting each own their Campaign variant, integrated by events and ACLs, not shared entities.