API Gateway
Three structural patterns in one system. Proxy guards the door, Adapter translates protocols, and Facade hides the mess of microservices behind a single clean endpoint.
Key Abstractions
Proxy that intercepts all client requests and applies middleware before forwarding
Adapter interface wrapping different backend protocols behind a uniform handle() method
Maps route paths to their target service adapters for request routing
Pluggable request processor for auth, rate limiting, logging, and caching
Facade that orchestrates calls to multiple services and returns a single combined response
Class Diagram
The Key Insight
Most engineers think of an API Gateway as a reverse proxy. That is one third of the picture. The real design has three structural patterns stacked on top of each other, each solving a different problem.
The Proxy Pattern handles the "invisible wall" between clients and services. Clients call the gateway using the exact same HTTP interface they would use to call the backend directly. They have no idea auth checks, rate limiting, and caching are happening. That transparency is the whole point of a proxy.
The Adapter Pattern solves protocol heterogeneity. Your microservices are not uniform. One team built their service with REST. Another uses gRPC. The legacy inventory system still speaks SOAP because nobody wants to rewrite it. Without adapters, your gateway needs protocol-specific routing logic for every backend. Adapters wrap each service behind a single handle(request) interface. The gateway routes uniformly.
The Facade Pattern handles response aggregation. A mobile client showing an order details screen needs data from three services. Making three separate API calls over a cellular connection is slow and fragile. The facade makes one call to the gateway, the gateway fans out to three services on a fast internal network, and returns one combined payload.
Requirements
Functional
- Route incoming requests to the correct backend service based on path
- Support multiple backend protocols (REST, RPC, SOAP/XML) through a uniform interface
- Apply a configurable middleware pipeline: authentication, rate limiting, logging, caching
- Aggregate responses from multiple services into a single response for the client
- Register and deregister services at runtime without code changes
Non-Functional
- Middleware execution order must be deterministic and configurable
- Rate limiting should be per-client, not global
- Cache should only apply to GET requests with 200 responses
- Partial failure in aggregation should return available data, not a blanket error
Design Decisions
Why Proxy for the gateway instead of a simple router?
A router forwards requests. That is all it does. A proxy forwards requests while adding behavior that is invisible to both sides. The client sends the same HTTP request it would send to the backend directly. The backend receives what looks like a normal incoming request. Neither side knows about auth checks, rate limiting, request logging, or response caching happening in between.
This matters because cross-cutting concerns multiply fast. Today it is auth and logging. Next quarter it is request tracing, header enrichment, and A/B routing. The proxy pattern lets you stack these transparently. A simple router would force you to either push this logic into every service (duplication) or make clients aware of it (coupling).
Why Adapter instead of making all services speak the same protocol?
You do not control legacy services. The inventory system was built in 2008 and speaks SOAP. Rewriting it is a six-month project nobody will fund. The payments team chose gRPC for performance and they are not switching.
The Adapter pattern wraps each backend behind the same handle(request) -> response interface. The gateway does not care if a service speaks REST, RPC, or carrier pigeon. It calls handle() and gets a Response back. When a new team builds a GraphQL service, you write one new adapter class. Nothing else changes.
Why Facade for aggregation instead of client-side orchestration?
If the mobile app calls OrderService, then PaymentService, then UserService to render one screen, that is three round trips over a cellular connection. Each one adds latency and another failure point.
The ResponseAggregator is a facade that takes a list of paths, calls each service on the fast internal network, and merges everything into one response. The client makes one call. Three round trips become one. And if PaymentService is down, the facade returns user and order data with a clear error for the payment portion instead of failing entirely.
Why a middleware chain instead of putting all checks in the Gateway class?
Imagine auth, rate limiting, logging, and caching all inside the Gateway's handle method. That is four responsibilities in one class. Adding request tracing means editing the Gateway. Removing caching means editing the Gateway. Every change risks breaking something unrelated.
A middleware chain separates each concern into its own class. Each middleware does exactly one thing and calls next_handler to pass control forward. You can add, remove, or reorder middlewares by changing a list. The Gateway class itself stays untouched. Need request tracing next sprint? Write a TracingMiddleware and insert it. Done.
Interview Follow-ups
- "How would you handle service discovery?" Replace the static ServiceRegistry with a dynamic one backed by Consul or etcd. Services register themselves on startup and deregister on shutdown. The gateway resolves routes at request time instead of boot time.
- "How would you add circuit breaking?" Wrap each ServiceAdapter in a CircuitBreakerAdapter that tracks failure rates. After a threshold, the circuit opens and returns a fallback response without calling the backend. This prevents cascading failures when a downstream service is struggling.
- "How would you handle WebSocket connections?" WebSockets need a persistent connection, not request-response routing. Add a WebSocketProxy alongside the HTTP Gateway that upgrades connections and maintains session affinity to the correct backend. The middleware pipeline still applies on the initial handshake.
- "How would you implement canary deployments?" Add a RoutingMiddleware that reads a percentage header or user segment and forwards to either the stable or canary adapter for the same service. The ServiceRegistry holds both adapters under different keys, and the middleware picks one based on the routing rules.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify scope and lock the process
GoalPin the three-pattern framing (Proxy + Adapter + Facade), name the middleware-order rule, decide budget.
Do & Say- SAY·1SAY: Gateway is three structural patterns stacked: Proxy for cross-cutting concerns, Adapter for protocol heterogeneity (REST/RPC/SOAP), Facade for fan-out aggregation. The interesting choice is how they compose. Aligned?
- SAY·2Lock scope: path-based routing, middleware pipeline (auth, rate limit, logging, cache), three protocol adapters, and response aggregation for mobile clients. Park service discovery, circuit breaking, WebSockets, and canary as v2.
- SAY·3Pin middleware order: Deterministic. Auth before rate-limit so unauthenticated requests don't consume rate budget. Cache after rate-limit so a cache hit still counts against the limit, keeping the limiter honest.
- SAY·4State cache scope: GET-only, 200-status only, keyed by path. POST/PUT/DELETE pass through. Per-path TTL is v2.
- SAY·5State the partial-failure rule for aggregation: If one of three services fails, the facade returns the other two plus an error stub. Not a blanket 500.
- ASK·6ASK: Sketch a quick Gateway/Adapter/Middleware/Aggregator diagram first, or jump into code? Your call.
Interviewer is grading: You name Proxy, Adapter, Facade together in the first 90 seconds as the three-pattern framing. You commit to a specific middleware order (auth before rate-limit) with a justification. You raise partial-failure handling for aggregation unprompted.
- 25-10 min
Sketch the API and (optionally) the class diagram
GoalLock the public method signatures and walk through one concrete request flow so the inside-out middleware chain isn't a surprise during coding.
Do & Say- SAY·1Name abstractions: Request, Response, ServiceAdapter (Rest/Rpc/Legacy), ServiceRegistry, Middleware (Auth/RateLimit/Logging/Cache), ResponseAggregator, Gateway.
- WRITE·2WRITE ServiceAdapter: handle(request) -> Response. One method. Gateway never knows the backend protocol; each adapter translates Request into its own protocol and back.
- WRITE·3WRITE Middleware: process(request, next_handler) -> Response. Rule: calling next forwards, not calling short-circuits. Examples: Auth 401s on missing header, Cache returns cached on hit, and RateLimit 429s when at limit.
- WRITE·4WRITE ServiceRegistry: register(path, adapter), get_adapter(path) with exact match then prefix match. Prefix match means /users/123 lands on /users without per-id registration.
- WRITE·5WRITE Gateway.handle: innermost is forward(req): look up adapter, return adapter.handle. Then: for each middleware in reversed order, wrap: handler = lambda req: mw.process(req, prev). Finally: call handler(request).
- SAY·6Walk one request, GET /users, order [Logging, Auth, RateLimit, Cache]: Logging prints, Auth passes, RateLimit appends timestamp, Cache misses, and forward hits RestAdapter, returns 200. Then: Cache stores, chain unwinds.
- WRITE·7WRITE ResponseAggregator: aggregate(paths, gateway, base_request). Steps: loops paths, builds Request with copied headers, calls gateway.handle, collects results, and failed become error stubs. Each fan-out runs the full pipeline including auth.
- DRAW·8DRAW (if requested): Gateway at top with Registry, Middleware list, and arrow to Aggregator, Registry to three Adapters, and Middleware interface with four implementations. Tag: Proxy, Adapter, Facade, and Chain of Responsibility on the clusters.
Interviewer is grading: Middleware chain is built inside-out by wrapping in reversed order. AuthMiddleware, RateLimitMiddleware, CacheMiddleware each demonstrate the short-circuit-by-not-calling-next pattern. Aggregator reuses the gateway's full pipeline, not a backdoor.
- 325 min
Code in this sequence (bottom-up)
GoalType the code in the same order the existing implementation builds it: Request/Response, ServiceAdapter family, Registry, Middleware family, Aggregator, Gateway, demo. Talk while you code.
Do & Say- SAY·1Start with Request/Response dataclasses. Request fields: path, method=GET, headers, body, params. Response fields: status_code, body, headers. SAY: Lingua franca. Every adapter and middleware speaks Request in, Response out. (~2 min)
- SAY·2Code ServiceAdapter abstract base. Then RestAdapter (JSON payload), RpcAdapter (RPC procedure name), LegacyAdapter (SOAP envelope). Same contract, different translations. (~5 min)
- SAY·3Code ServiceRegistry. _routes dict, register, get_adapter with exact match then prefix-match loop. Prefix match is what makes /users/123 work without registering every ID. (~2 min)
- SAY·4Code Middleware base, then the four implementations: AuthMiddleware (401 if no header), RateLimitMiddleware (per-client timestamp lists, evict old, 429 at limit), LoggingMiddleware (print before/after), and CacheMiddleware (GET-only, path-keyed, store on 200). Short-circuit = don't call next. (~7 min)
- SAY·5Code ResponseAggregator. Loop paths, fresh Request per path with copied headers, call gateway.handle, parse JSON on 200 or error stub. Returns merged Response. Goes through full pipeline including auth. (~3 min)
- SAY·6Code Gateway. Constructor: Registry plus empty middleware list. handle defines forward(req) (lookup adapter, 404 if missing). Loop middlewares in reversed order, wrap with _wrap helper. Reversed iteration puts first-registered outermost. (~4 min)
- SAY·7Demo registrations: /users RestAdapter, /payments RpcAdapter, and /inventory LegacyAdapter. Add [Logging, Auth, RateLimit(3,60), Cache]. Runs: auth GET /users (200), unauth (401), /payments (200), /inventory (200), repeat /users (cache hit), 4th /payments (429). Then aggregate /users+/payments. (~2 min)
- SAY·8Mental walk: GET /payments with valid auth, 4th call. Logging prints. Auth passes. RateLimit sees 3 timestamps, returns 429. Cache and Adapter never run. Short-circuit works. (~1 min)
Interviewer is grading: Middleware chain built with reversed iteration so registration order matches execution order. Each middleware short-circuits by not calling next. RestAdapter, RpcAdapter, LegacyAdapter all implement the same handle signature with different payload shapes. Aggregator reuses Gateway.handle, not a backdoor.
- 45 min
Trade-offs, extensions, and wrap-up
GoalDefend the middleware order, defend Adapter over protocol-specific routing, volunteer circuit breaking, anticipate service discovery, close in one sentence.
Do & Say- SAY·1Trade-off one, auth before rate-limit: Rate-limit-first lets an unauthenticated attacker exhaust the budget by spamming /anything. Auth-first rejects with 401 before counting. Per-client rate limiting keyed by Authorization header requires auth first.
- SAY·2Trade-off two, Adapter over uniform protocols: We don't control legacy services. Inventory's 2008 SOAP isn't getting rewritten, Payments picked gRPC. Adapter wraps each backend behind handle(request). New GraphQL service = one new adapter, zero edits elsewhere.
- SAY·3Volunteer circuit breaking: Wrap each ServiceAdapter in a CircuitBreakerAdapter tracking failure rates. Past threshold, circuit opens, returns 503 fallback without calling backend. Lives between registry and adapter, transparent to middleware.
- WATCH·4Anticipated follow-up on service discovery: Replace static ServiceRegistry with one backed by Consul or etcd. Services register on startup. Gateway resolves at request time. Adapter interface unchanged, only registry lookup swaps.
- SAY·5Close: Gateway as Proxy with middleware built inside-out via reversed wrapping. ServiceAdapter for REST/RPC/SOAP. ServiceRegistry with prefix match. ResponseAggregator as Facade reusing the pipeline. Deterministic middleware order with auth first.
Interviewer is grading: You defend auth-before-rate-limit with the credential-spam attack. You volunteer circuit breaking unprompted. You explain service discovery with one sentence using the registry hook.
Code Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Callable
import json
import time
@dataclass
class Request:
path: str
method: str = "GET"
headers: dict = field(default_factory=dict)
body: str = ""
params: dict = field(default_factory=dict)
@dataclass
class Response:
status_code: int
body: str
headers: dict = field(default_factory=dict)
# --------------- Service Adapters (Adapter Pattern) ---------------
class ServiceAdapter(ABC):
@abstractmethod
def handle(self, request: Request) -> Response:
...
class RestAdapter(ServiceAdapter):
"""Adapter for REST-based backend services."""
def __init__(self, service_name: str):
self.service_name = service_name
def handle(self, request: Request) -> Response:
# Simulates translating to REST and calling the backend
payload = {"source": self.service_name, "protocol": "REST",
"method": request.method, "path": request.path}
return Response(200, json.dumps(payload))
class RpcAdapter(ServiceAdapter):
"""Adapter for gRPC-style RPC backend services."""
def __init__(self, service_name: str):
self.service_name = service_name
def handle(self, request: Request) -> Response:
# Translates the standard request into an RPC call format
payload = {"source": self.service_name, "protocol": "RPC",
"procedure": f"{request.method}_{request.path.strip('/')}"}
return Response(200, json.dumps(payload))
class LegacyAdapter(ServiceAdapter):
"""Adapter for SOAP/XML legacy backend services."""
def __init__(self, service_name: str):
self.service_name = service_name
def handle(self, request: Request) -> Response:
# Wraps the request into a SOAP envelope before forwarding
soap_body = (f"<Envelope><Body><{self.service_name}Request>"
f"<path>{request.path}</path>"
f"</{self.service_name}Request></Body></Envelope>")
payload = {"source": self.service_name, "protocol": "SOAP",
"translated_body": soap_body}
return Response(200, json.dumps(payload))
# --------------- Service Registry ---------------
class ServiceRegistry:
def __init__(self):
self._routes: dict[str, ServiceAdapter] = {}
def register(self, path: str, adapter: ServiceAdapter) -> None:
self._routes[path] = adapter
def get_adapter(self, path: str) -> ServiceAdapter | None:
# Exact match first, then prefix match
if path in self._routes:
return self._routes[path]
for route, adapter in self._routes.items():
if path.startswith(route):
return adapter
return None
# --------------- Middleware (Chain of Responsibility) ---------------
# A handler is anything callable that takes a Request and returns a Response
Handler = Callable[[Request], Response]
class Middleware(ABC):
@abstractmethod
def process(self, request: Request, next_handler: Handler) -> Response:
...
class AuthMiddleware(Middleware):
def process(self, request: Request, next_handler: Handler) -> Response:
if "Authorization" not in request.headers:
return Response(401, "Unauthorized: missing Authorization header")
return next_handler(request)
class RateLimitMiddleware(Middleware):
def __init__(self, max_requests: int = 5, window_seconds: float = 60.0):
self.max_requests = max_requests
self.window_seconds = window_seconds
self._counts: dict[str, list[float]] = {}
def process(self, request: Request, next_handler: Handler) -> Response:
client = request.headers.get("Authorization", "anonymous")
now = time.time()
timestamps = self._counts.setdefault(client, [])
# Evict old timestamps outside the window
timestamps[:] = [t for t in timestamps if now - t < self.window_seconds]
if len(timestamps) >= self.max_requests:
return Response(429, "Rate limit exceeded")
timestamps.append(now)
return next_handler(request)
class LoggingMiddleware(Middleware):
def process(self, request: Request, next_handler: Handler) -> Response:
print(f" [LOG] {request.method} {request.path}")
response = next_handler(request)
print(f" [LOG] Response status: {response.status_code}")
return response
class CacheMiddleware(Middleware):
def __init__(self):
self._cache: dict[str, Response] = {}
def process(self, request: Request, next_handler: Handler) -> Response:
if request.method == "GET":
cache_key = request.path
if cache_key in self._cache:
print(f" [CACHE] Hit for {cache_key}")
return self._cache[cache_key]
response = next_handler(request)
if response.status_code == 200:
self._cache[cache_key] = response
return response
return next_handler(request)
# --------------- Response Aggregator (Facade Pattern) ---------------
class ResponseAggregator:
def aggregate(self, paths: list[str], gateway: "Gateway",
base_request: Request) -> Response:
results = {}
for path in paths:
req = Request(path=path, method="GET",
headers=dict(base_request.headers))
resp = gateway.handle(req)
if resp.status_code == 200:
try:
results[path] = json.loads(resp.body)
except json.JSONDecodeError:
results[path] = resp.body
else:
results[path] = {"error": resp.body, "status": resp.status_code}
return Response(200, json.dumps(results, indent=2))
# --------------- Gateway (Proxy Pattern) ---------------
class Gateway:
def __init__(self):
self.registry = ServiceRegistry()
self._middlewares: list[Middleware] = []
def add_middleware(self, middleware: Middleware) -> None:
self._middlewares.append(middleware)
def handle(self, request: Request) -> Response:
# Build the middleware chain from inside out.
# The innermost handler forwards to the actual service adapter.
def forward(req: Request) -> Response:
adapter = self.registry.get_adapter(req.path)
if adapter is None:
return Response(404, f"No service registered for {req.path}")
return adapter.handle(req)
handler = forward
for mw in reversed(self._middlewares):
# Capture mw in closure properly
handler = self._wrap(mw, handler)
return handler(request)
@staticmethod
def _wrap(mw: Middleware, next_handler: Handler) -> Handler:
def wrapped(req: Request) -> Response:
return mw.process(req, next_handler)
return wrapped
if __name__ == "__main__":
# 1. Create gateway and register services with different adapters
gw = Gateway()
gw.registry.register("/users", RestAdapter("UserService"))
gw.registry.register("/payments", RpcAdapter("PaymentService"))
gw.registry.register("/inventory", LegacyAdapter("InventoryService"))
# 2. Add middleware chain: logging -> auth -> rate_limit -> cache
# Execution order: logging first, then auth, then rate limit, then cache
gw.add_middleware(LoggingMiddleware())
gw.add_middleware(AuthMiddleware())
gw.add_middleware(RateLimitMiddleware(max_requests=3, window_seconds=60))
gw.add_middleware(CacheMiddleware())
print("=== Test 1: Authenticated GET /users (REST adapter) ===")
resp = gw.handle(Request(path="/users", method="GET",
headers={"Authorization": "Bearer token123"}))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 2: Unauthenticated request (rejected by auth) ===")
resp = gw.handle(Request(path="/users", method="GET"))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 3: Authenticated GET /payments (RPC adapter) ===")
resp = gw.handle(Request(path="/payments", method="GET",
headers={"Authorization": "Bearer token123"}))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 4: Authenticated GET /inventory (Legacy SOAP adapter) ===")
resp = gw.handle(Request(path="/inventory", method="GET",
headers={"Authorization": "Bearer token123"}))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 5: Cache hit on repeated GET /users ===")
resp = gw.handle(Request(path="/users", method="GET",
headers={"Authorization": "Bearer token123"}))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 6: Rate limit exceeded (4th request, limit is 3) ===")
resp = gw.handle(Request(path="/payments", method="GET",
headers={"Authorization": "Bearer token123"}))
print(f" Status: {resp.status_code}")
print(f" Body: {resp.body}\n")
print("=== Test 7: Facade aggregation - /users + /payments in one call ===")
aggregator = ResponseAggregator()
# Use a fresh gateway without rate limit for clean aggregation demo
gw2 = Gateway()
gw2.registry.register("/users", RestAdapter("UserService"))
gw2.registry.register("/payments", RpcAdapter("PaymentService"))
gw2.add_middleware(LoggingMiddleware())
gw2.add_middleware(AuthMiddleware())
gw2.add_middleware(CacheMiddleware())
base = Request(path="/", headers={"Authorization": "Bearer token123"})
combined = aggregator.aggregate(["/users", "/payments"], gw2, base)
print(f" Status: {combined.status_code}")
print(f" Combined body:\n{combined.body}")Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Reaches a working router with one or two middleware checks, but Adapter, Facade, and chain composition stay vague.
- Identifies that the gateway routes requests to backend services based on path.
- Adds an auth check before forwarding to the backend.
- Recognizes that different backends speak different protocols when prompted.
- Picks 'a list of middleware' for stacking cross-cutting concerns.
- Stores a route-to-handler map for routing.
- Hardcodes service URLs inside the gateway instead of using a registry.
- Puts auth and rate-limit logic inline in handle() instead of as separate middleware.
- Misses the auth-before-rate-limit ordering and lets unauthenticated clients consume the rate budget.
- Implements the cache as a global dict without GET-only or status-code guards.
- Models aggregation as 'call each service in a loop in the client' instead of a Facade on the gateway.
Mid-Level Engineer (L4)
Drives the design end-to-end with a working middleware chain, three protocol adapters, and a Facade aggregator that reuses the pipeline.
- Builds the middleware chain via reversed-iteration wrapping so registration order equals execution order.
- Implements three Adapters (REST, RPC, SOAP) all returning the same Response shape.
- Implements AuthMiddleware, RateLimitMiddleware, CacheMiddleware, LoggingMiddleware each with a short-circuit-by-not-calling-next path.
- Implements RateLimitMiddleware as a sliding-window with per-client (per-Authorization-header) timestamp lists.
- Implements CacheMiddleware as GET-only, 200-only, keyed by path.
- Builds ResponseAggregator that calls gateway.handle for each path so the full pipeline runs per fan-out call.
- Uses ServiceRegistry.get_adapter with exact match plus prefix match fallback.
- Does not volunteer circuit breaking or canary routing until asked.
- Misses service discovery as the replacement for the static registry.
- Treats WebSockets as out of scope without sketching the upgrade-then-pipeline path.
Senior Engineer (L5+)
Volunteers circuit breaking and service discovery before being asked, names the middleware-order rationale, and frames each pattern around the failure it prevents.
- Volunteers circuit breaking (CircuitBreakerAdapter wrapping each ServiceAdapter) and canary routing (RoutingMiddleware based on a percentage header) unprompted.
- Names the auth-before-rate-limit invariant and explains the credential-spam attack it defends against.
- Frames the patterns around failure modes: Proxy prevents 'every service implements its own auth', Adapter prevents 'the gateway has a rewrite-the-legacy-system dependency', Facade prevents 'three cellular round trips for one mobile screen', Chain prevents 'adding a trace middleware means editing Gateway'.
- Defends the cache-after-rate-limit ordering with the 'cache hits should still count toward the limit' argument.
- Proposes service discovery via Consul / etcd to replace the static ServiceRegistry, with the adapter interface unchanged.
- Proposes WebSocket support as a parallel WebSocketProxy with the middleware pipeline running on the initial handshake only.
- Closes with a one-sentence summary covering Proxy, Adapter, Facade, the middleware chain, and the partial-failure aggregation rule in under 25 seconds.
Common Mistakes
- ✗Putting business logic in the gateway. The gateway routes and guards. Business logic belongs in the services.
- ✗Hardcoding service URLs. Use a registry so services can be added, removed, or moved without code changes.
- ✗Running middleware in the wrong order. Rate limiting before auth means unauthenticated users consume your rate limit budget.
- ✗Not handling partial failures in aggregation. If one of three services fails, the facade should return partial data, not crash.
Key Points
- ✓The Gateway is a proxy: same interface as backend services, but with added cross-cutting concerns.
- ✓Adapters let you integrate REST, RPC, and legacy SOAP services without the gateway knowing the difference.
- ✓Middleware chain is ordered. Auth runs first, then rate limit, then cache check, then forward.
- ✓Facade aggregation means clients make one call instead of orchestrating three service calls themselves.