backend / microservices / 02_inter_service_communication.md

Inter-service Communication

5 interview angles 4 min read source

Inter-service Communication

Two big buckets: synchronous (caller waits) and asynchronous (fire-and-forget through a broker). The first interview question is almost always “when do you pick which?”

Synchronous: HTTP/REST and gRPC

Caller blocks on a response. Coupling is temporal — both services must be up at the same time.

REST/JSON over HTTP

import httpx

async def get_user(user_id: int) -> dict:
    async with httpx.AsyncClient(timeout=2.0) as c:
        r = await c.get(f"http://users/v1/users/{user_id}")
        r.raise_for_status()
        return r.json()

Pros: ubiquitous, debuggable (curl), browser-friendly, easy versioning. Cons: verbose payloads, no schema enforcement unless you add OpenAPI, slower than binary protocols.

gRPC

Protobuf-defined contracts, HTTP/2, bidi streaming.

# Stub generated from orders.proto
import grpc, orders_pb2, orders_pb2_grpc

async def place_order(order):
    async with grpc.aio.insecure_channel("orders:50051") as ch:
        stub = orders_pb2_grpc.OrdersStub(ch)
        return await stub.Place(orders_pb2.PlaceRequest(...))

Pros: binary + smaller, strong schema, streaming, deadlines built in, polyglot. Cons: browser support requires gRPC-Web, less human-debuggable, harder local testing.

Rule of thumb

REST for external APIs, partner integrations, browser-facing. gRPC for internal service-to-service where latency and contracts matter.

Asynchronous: message brokers and events

Caller publishes a message; consumer processes later. Spatial coupling stays, temporal coupling drops — the consumer can be down for a minute and you don’t care.

Command vs Event

Command Event
Intent “do this” “this happened”
Coupling sender knows the receiver sender doesn’t know consumers
Naming SendEmail, ChargeCard (imperative) OrderPlaced, PaymentSucceeded (past tense)
Queue type typically work queue (SQS, Celery) typically topic/pub-sub (SNS, Kafka, RabbitMQ topic exchange)

Mixing the two is a classic distributed-system smell. If consumers are explicitly listed in the sender’s code, it’s a command pretending to be an event.

Pattern: pub/sub with SNS + SQS

publisher → SNS topic ─┬─> SQS queue (orders consumer)
                       ├─> SQS queue (analytics consumer)
                       └─> SQS queue (audit consumer)

Each consumer has its own queue subscription, retries independently, and can dead-letter without affecting the others. This is the AWS canonical pattern.

Pattern: Kafka topic

Same idea but consumers pull at their own pace, can replay, and ordering is preserved per-partition. See backend/10_message_queues/kafka/.

The synchronous-async decision matrix

Situation Sync (HTTP/gRPC) Async (queue/event)
Caller needs the answer NOW (UI, search) yes no
One operation, several downstream side effects no yes (fan-out)
Result not needed in-band (“send confirmation email”) no yes
Failures should retry without involving the caller no yes
Caller and callee have different scaling profiles no yes (queue buffers)
Strong consistency required yes no (eventual)
Two services in tight loop — chatty minimize minimize either way

Anti-patterns

  • Synchronous chain ≥ 4 services deep. Latency multiplies, every link is a failure point. Refactor to async fan-out or merge services.
  • HTTP “events” via polling. If you find yourself polling /orders?since=..., you want a queue/topic.
  • Distributed monolith via shared library. Services that can’t deploy independently are a monolith with extra steps.
  • Async for in-line UX. If the user is waiting on a button, async + “we’ll email you” works; async + UI silently waiting doesn’t.

Timeouts, retries, backoff

Mandatory for every sync call. Defaults are not defensible.

async with httpx.AsyncClient(
    timeout=httpx.Timeout(connect=1.0, read=2.0, write=2.0, pool=5.0)
) as c:
    ...

Retry rules:

  • Only retry idempotent operations (GET, PUT with idempotency key, POSTs with explicit Idempotency-Key header).
  • Exponential backoff with jitter — min(cap, base * 2**attempt) + random(0, base).
  • Retry 5xx and connect errors; don’t retry 4xx (the bug is on your end).
  • Cap total attempts (3 is common).

Circuit breaker

When the dependency is dying, stop hammering it. Three states:

CLOSED  (normal) ──failures over threshold──> OPEN  (fast-fail, no calls)
              ▲                              │
              │                          after cooldown
              │                              │
              └──── HALF_OPEN ◄──────────────┘
                  (probe; success → CLOSED, fail → OPEN)

Libraries: purgatory, pybreaker, or the breaker inside tenacity/stamina. Often the sidecar (Envoy/Istio) does this for you transparently.

Schema evolution and versioning

Async messages live longer than the code that emitted them. Rules:

  • Add fields, don’t rename or remove for a long deprecation window.
  • Use Protobuf or Avro with a schema registry for strict cross-team contracts.
  • For JSON: tolerant readers (ignore unknown fields) + producers that never omit existing fields.
  • Version in the URL path for sync (/v1/...), in the message envelope for async (event_version: 2).

Interview angle

  • “Sync vs async — when each?” — sync when the caller needs the answer in the same request; async when the work is a side-effect, or the dependency can be slow/unreliable, or you need fan-out.
  • “You have Order → Payment → Inventory → Email all over HTTP. What’s wrong?” — synchronous chain; tail-latency multiplies; one slow service kills the user request. Refactor: Order publishes OrderPlaced; payment, inventory, email subscribe.
  • “How do you retry safely?” — only on idempotent ops; exponential backoff with jitter; don’t retry 4xx; cap attempts.
  • “REST or gRPC for internal services?” — gRPC where you control both ends and latency matters; REST for external/partner APIs and ad-hoc debugging.
  • “What does a circuit breaker do that retries don’t?” — retries protect a single call from a transient blip; breaker protects the dependency (and your threadpool) from collapse during a sustained outage.