Structural Patterns in Microservices
Structural Patterns in Microservices maps GoF structural patterns onto network boundaries.
Introduction
Structural Patterns in Microservices maps GoF structural patterns onto network boundaries. Anti-Corruption Layer is Adapter for upstream services. API Gateway is Facade + Proxy. Service mesh sidecar is transparent Proxy. Aggregator is Composite. Mesh filters are Decorators. Same vocabulary — new failure modes (latency, partial failure, ownership).
The story
An e-commerce team split a monolith into 14 services. Clients saw requests per page climb from 1 to 27. An aggregator (Composite + Facade) collapsed "view product" to one round trip; sidecar Proxies added retries and TLS uniformly.
The business problem
Microservice splits without structural boundaries create client and ops pain:
- Chatty clients: 22+ calls per screen — mobile battery and latency suffer.
- Foreign vocabulary: legacy ERP terms leak into every consumer.
- Repeated cross-cutting: auth, rate-limit, mTLS reimplemented per service.
- Client-specific shapes: mobile, web, partner need different aggregations.
The problem teams faced
Apply structural patterns at boundaries when:
- Clients suffer N+1 round trips across services.
- Upstream services expose domain-foreign vocabulary.
- Operational concerns must be uniform without editing every service.
- Different client types need different aggregated response shapes.
Understanding the topic
Intent: Apply Adapter, Facade, Proxy, Decorator, Composite across service boundaries.
- API Gateway (Facade + Proxy) — single entry; routes, auth, rate-limits.
- ACL (Adapter) — translates between bounded contexts / legacy.
- BFF (Facade) — per-client aggregator returning shaped responses.
- Sidecar (Proxy) — mTLS, retries, metrics transparently.
- Aggregator (Composite) — assembles tree of service responses.
Internal architecture
Structural layers at service boundary:
Clients│▼API Gateway (Facade + Proxy — auth, route, rate-limit)│▼Mobile BFF (Facade — client-specific aggregation)│├── Catalog Service├── Cart Service└── ACL → Legacy ERP (Adapter — domain ↔ vendor translation)│[Envoy sidecar] (Proxy — mTLS, retries, metrics)
Visual explanation
Three diagrams map GoF patterns to microservice layers:
Informative example
Before / after — chatty client to BFF + ACL:
// ❌ Mobile app orchestrates 22 service calls per home screenconst catalog = await fetch("/catalog/featured")const cart = await fetch("/cart/summary")const recs = await fetch("/recommendations/user")// ... 19 more — 1.9s p95// ✅ Mobile BFF — one call, server-side Facade + Composite// GET /mobile-bff/homeclass MobileHomeBff {async getHome(userId: string) {const [catalog, cart, recs] = await Promise.all([this.catalog.featured(),this.cart.summary(userId),this.recs.forUser(userId),])return { catalog, cart, recs } // mobile-shaped DTO}}// ACL wraps legacy ERP — consumers never see ERP typesclass ErpInventoryAdapter implements InventoryPort {async reserve(sku: string, qty: number) {const erpResponse = await this.erp.POST_INVENTORY_HOLD({ SKU: sku, QTY: qty })return mapToDomainReservation(erpResponse) // Adapter translation}}
Execution workflow
Identify pain
Chattiness, foreign vocabulary, repeated ops concerns.
Real-world use
Netflix Zuul/Spring Cloud Gateway, Kong, AWS API Gateway; Istio/Envoy/Linkerd sidecars; GraphQL gateways (Composite); Stripe expand parameter; Strangler-Fig migrations with ACLs.
Production case study
Mobile home screen BFF:
- Scenario: Mobile app 22 calls per home screen; p95 1.9s.
- Decision: Mobile BFF + aggregator; deprecate direct service calls.
- Outcome: p95 460ms; one client retry policy.
- Lesson: each layer is a hop — budget latency deliberately.
Trade-offs
- Pro: one place per cross-cutting concern; coherent client API.
- Pro: internal services evolve without breaking clients.
- Con: each layer adds hop — latency budget shrinks.
- Con: gateways/BFFs can become new monoliths without governance.
Decision framework
- Use BFF when multiple clients need different response shapes.
- Use ACL when legacy upstream vocabulary must not leak.
- Use sidecar when ops concerns must be uniform across languages.
- Avoid layers when single client and few services — direct calls simpler.
Best practices
- ACL owned by calling bounded context — one service, not scattered adapters.
- BFF per client type (mobile, web) — not one BFF for everything.
- Aggregator composes — if business logic appears, push down to owning service.
- Latency budget per layer documented in ADR.
Anti-patterns to avoid
- God BFF with business rules for all domains.
- ACL copy-pasted into every consumer instead of shared service.
- Gateway that becomes transaction script for entire platform.
Common mistakes
- BFF fan-out to all 40 services for any request.
- Sidecar memory/latency cost ignored in capacity planning.
- No ownership — gateway team becomes bottleneck for all features.
Debugging tips
- Distributed trace across gateway → BFF → services → ACL.
- Count fan-out per BFF endpoint — alert on growth.
Optimization strategies
- Parallel fan-out in BFF where dependencies independent.
- Cache aggregated responses with short TTL for read-heavy screens.
Common misconceptions
- BFF ≠ API Gateway. Gateway is shared infra; BFF is client-specific Facade.
- Sidecar is Proxy — not a second copy of business logic.
- GraphQL gateway is Composite + Facade at field level.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionBFF vs Gateway?+
Answer
Follow-up
2AdvancedQuestionWhere does ACL live and who owns it?+
Answer
Follow-up
3IntermediateQuestionSidecar vs in-process library?+
Answer
Follow-up
4AdvancedQuestionWhen aggregator becomes problem?+
Answer
Follow-up
Summary
You can map Adapter/Facade/Proxy/Composite to gateways, BFFs, ACLs, and sidecars — and explain trade-offs at network scale. Tell the 27-requests-per-page story — if you can do that, you own the Structural section.