KAHIBARO
Discord Login Register

27.9. Distributed APIs

Why Distributed APIs?

Modern backend systems often grow beyond a single application and a single server. You might have:

All of this creates a distributed API environment. In simple terms:

A distributed API is an API that is implemented and served by multiple services, components, or locations, but should still look like a single coherent API to clients.

This chapter focuses on the practical problems and patterns that are specific to distributed APIs, not on basic REST or HTTP, which you learned earlier.

Typical examples:

The main challenges are:

Types of Distributed API Setups

1. Single API, Multiple Internal Services

From the client side, there is a single API, for example:

text
https://api.example.com

Internally, multiple services handle different parts:

ServiceInternal URLPublic paths (through API gateway)
Auth servicehttp://auth-service:8000/auth/*
User servicehttp://user-service:8000/users/*
Product servicehttp://product-service:8000/products/*
Order servicehttp://order-service:8000/orders/*

A reverse proxy or API gateway forwards requests to the right service.

Characteristics:

2. Multiple APIs, Single Client Application

Sometimes one client (a web app or mobile app) calls several APIs directly:

Characteristics:

This is simpler on the backend, but pushes complexity to the client.

3. Multi‑Region APIs

A company might run the same API in different regions:

Or use one global domain with smart routing (using DNS or an API gateway) to the nearest region.

Challenges:

4. Internal Distributed APIs

Within your company, many internal systems talk to each other via APIs, even if there is no public API. For instance:

These are distributed APIs too, just not exposed to external users. They still face the same challenges: versioning, contracts, reliability, and so on.

API Gateway and Aggregation

What an API Gateway Does

An API gateway sits in front of multiple services and provides a unified entry point.

Common responsibilities:

Examples of gateways / reverse proxies:

Table of typical concerns:

ConcernWhere it often lives in distributed APIs
RoutingAPI Gateway / Reverse Proxy
Authn/AuthzAPI Gateway and/or shared middleware
Business logicIndividual backend services
Data accessIndividual backend services
Rate limitingAPI Gateway, service mesh, or per service

Aggregation: One Request, Many Services

Some API operations need data from multiple services. For example, an endpoint:

http
GET /me

might need:

You have 2 main options.

1. Aggregation in the Client

The client calls multiple endpoints:

  1. GET /users/me
  2. GET /users/me/settings
  3. GET /orders?user_id=me&limit=5

Pros:

Cons:

2. Aggregation in a Backend Gateway or “BFF”

A Backend For Frontend (BFF) is a service that aggregates data for a specific client type.

Example BFF endpoint:

http
GET /me

BFF code (pseudocode / Python style):

python
async def get_me(user_id: int):
    user = await http_get(f"http://user-service/users/{user_id}")
    settings = await http_get(f"http://settings-service/users/{user_id}/settings")
    orders = await http_get(
        f"http://order-service/orders?user_id={user_id}&limit=5"
    )
    return {
        "user": user,
        "settings": settings,
        "recent_orders": orders,
    }

Pros:

Cons:

Consistency and Data Ownership

Distributed APIs usually work on distributed data. The hard question is: who owns what?

Clear Data Ownership

In a distributed system, each service should have clear ownership of some data.

Example:

ServiceData it owns
User serviceUser profiles, emails, passwords
Product serviceProduct catalog, prices, categories
Order serviceOrders, order status, invoices

Rule of thumb: Only the owner service can change its data. Other services must call the owner via an API or read replicas.

This prevents:

Consistency Models in Distributed APIs

Two important ideas:

In a single database, strong consistency is easier. In a distributed system, you often accept eventual consistency.

Examples of eventual consistency in APIs:

Important rule: In distributed APIs, you usually cannot guarantee that every client sees the latest data instantly everywhere. Design your API and UX to tolerate eventual consistency.

Example: User Email Change Flow

Imagine this flow:

  1. User calls PATCH /users/me with { "email": "new@example.com" }.
  2. User service updates the user in its database.
  3. It publishes an event user_email_changed to a message queue.
  4. Other services (Billing, Notifications, etc.) listen and update their own data.

Consequences:

Your API contracts and documentation must make this clear: some derived or cached data may lag behind.

Distributed Transactions and Sagas

The Problem with Transactions Across Services

A normal database transaction ensures ACID properties inside one database. In distributed APIs, you might have:

A classic relational transaction cannot span all of these easily.

Example business action:

“Place an order” needs:
1. Create order record
2. Reserve stock
3. Charge payment

If any step fails, you want to roll back. But those steps happen in different systems.

Two‑Phase Commit vs Sagas

Distributed systems sometimes use two‑phase commit (2PC) across databases, but it is complex and can hurt availability, so it is less common in modern microservices.

Instead, many teams use the Saga pattern.

Saga Pattern Overview

A saga is a sequence of local transactions, each with a compensating action that can undo it.

Basic idea:

  1. Service A does step 1 and commits locally
  2. Service B does step 2 and commits locally
  3. Service C does step 3 and commits locally

If step 3 fails, you do compensating actions:

For our order example:

StepActionCompensating action
1Create order with status PENDINGSet order status to CANCELLED
2Reserve stock for itemsRelease stock
3Charge paymentRefund payment (if charge succeeded)

Two styles:

Simple Saga Example (Choreography)

  1. Client calls POST /orders on Order service.
  2. Order service creates order PENDING, publishes order_created.
  3. Stock service receives order_created, tries to reserve stock:
    • If success, publishes stock_reserved.
    • If failure, publishes stock_reservation_failed.
  4. Payment service receives stock_reserved, tries to charge card:
    • If success, publishes payment_succeeded.
    • If failure, publishes payment_failed.
  5. Order service listens:
    • On payment_succeeded, sets order CONFIRMED.
    • On stock_reservation_failed or payment_failed, sets order CANCELLED and triggers compensations.

Your public REST API for the client is simple:

http
POST /orders
GET /orders/{id}

But internally, the system uses events and compensations. The API is just the entry point to a long‑running, distributed process.

What This Means for API Design

A saga is not instantaneous. So your API must:

For example:

http
POST /orders
201 Created
Location: /orders/123
{
  "id": 123,
  "status": "PENDING"
}

The client can then poll:

http
GET /orders/123

Until status becomes CONFIRMED or CANCELLED.

This is much more realistic for distributed APIs than pretending everything is done inside one request/response.

Handling Failures, Timeouts, and Retries

In a distributed environment, failures are normal:

Your API must be designed to remain usable and predictable.

Timeouts and Circuit Breakers

If your service calls another service, you must set a timeout. Otherwise your API endpoint can hang and block resources.

Example in Python (simplified):

python
import httpx
async def call_order_service(order_id: int):
    async with httpx.AsyncClient(timeout=2.0) as client:
        return await client.get(f"http://order-service/orders/{order_id}")

If the call takes more than 2 seconds, it fails.

A circuit breaker adds another layer: if repeated calls fail, it stops sending requests for a while and returns an error immediately, to avoid overloading a failing service.

High‑level behavior of a circuit breaker:

You will usually use libraries or your API gateway to implement this.

Retries and Idempotency

When a call fails, you might retry. But retries can cause duplicate operations, for example double‑charging a payment.

So you must combine:

You learned about idempotency before. In distributed APIs it becomes critical.

Important rule: Any API operation that might be retried must be idempotent or protected with an idempotency key. This is essential in distributed systems.

Example for payments:

http
POST /payments
Idempotency-Key: 71a4f1...
{ "order_id": 123, "amount": 100.0 }

The server stores that it has processed key 71a4f1... already. If it receives the same key again, it returns the same result without creating a new payment.

Fallbacks and Partial Responses

When aggregating data from multiple services, some downstream calls may fail. You can:

Example partial response:

json
{
  "user": { "id": 1, "name": "Alice" },
  "settings": { "theme": "dark" },
  "recent_orders": null,
  "warnings": ["Recent orders are temporarily unavailable"]
}

Distributed API Versioning and Contracts

Distributed APIs have many services and many clients. You must manage change carefully.

Backward Compatibility First

Because you have many moving parts, it is easier to add than to change or remove.

Principles:

Examples of compatible changes:

ChangeSafe?
Add a new optional field in responseUsually safe
Add a new endpointSafe
Make a required field optionalUsually safe
Rename a fieldBreaking change
Change field type (string → number)Breaking change

Versioning Approaches

You learned API versioning before. In a distributed setting:

Common patterns:

The challenge is synchronizing version changes across many services.

Example: Gateway Version vs Service Version

The gateway can gradually route some traffic to /internal/v2/ while keeping public API stable. This is useful for canary releases and backward compatibility.

Contract Testing Between Services

In a distributed system, you should avoid breaking your internal consumers as well.

A contract test validates that a service still returns what its consumers expect.

High level idea:

This is especially helpful when different teams own different services.

Distributed Identity and Authorization

When you have multiple services, you cannot have each one do its own login and identity model independently. You need a consistent identity across your distributed APIs.

Central Authentication, Distributed Authorization

Typical setup:

Example JWT payload for a distributed system:

json
{
  "sub": "user-123",
  "email": "alice@example.com",
  "roles": ["user", "premium"],
  "permissions": ["orders:read", "orders:create"],
  "iat": 1692000000,
  "exp": 1692003600
}

All services can read the same sub (user ID) and permissions, and enforce their own authorization logic.

Propagating Identity Across Internal Calls

Suppose the API gateway receives a request with Authorization: Bearer <JWT>, validates it, and routes to the Order service. The Order service then calls the Payment service.

Questions:

Common patterns:

  1. User token propagation
    • Order service forwards the user’s token in internal calls.
    • Payment service knows who the end user is.
  2. Service‑to‑service tokens
    • Gateway validates user token and passes only user ID and claims in headers.
    • Order calls Payment with a service credential that identifies the Order service.
    • Payment service trusts Order and uses the passed user info to do its logic.

Table:

PatternProsCons
User token propagationEnd-to-end user identity availableMore complex token handling in all services
Service tokens + headersSimpler security model between servicesMust ensure headers cannot be forged

In both cases, your distributed APIs must consistently handle identity and permissions, or you risk security problems.

Observability in Distributed APIs

When an error happens in a distributed API, it can be hard to know where it went wrong. Good observability is essential.

Correlation IDs

A correlation ID is a unique ID attached to a whole request flow.

Example:

  1. API gateway receives request, generates X-Request-ID: 5f2c9e...
  2. Gateway forwards this header to all downstream services.
  3. Each service includes X-Request-ID in logs.

Now you can search logs in all services by this ID and see the path the request took.

Many teams also call this a trace ID when using distributed tracing tools.

Distributed Tracing

Tools like Jaeger, Zipkin, or cloud tracing services allow you to see:

They work by propagating trace headers, for example:

From an API design perspective, you must ensure that all services forward tracing headers and do not drop them.

Structured Logging Across Services

In distributed APIs, logs from many services must be combined. You learned about logging already, here the key point is:

Example JSON log entry:

json
{
  "timestamp": "2026-08-28T10:12:34Z",
  "service": "order-service",
  "level": "ERROR",
  "request_id": "5f2c9e...",
  "user_id": "user-123",
  "path": "/orders/123",
  "message": "Payment service timeout"
}

Designing Distributed API Operations

So far, we looked at individual concerns. Let us bring them together in a couple of concrete design patterns.

Pattern 1: Asynchronous Operations with Status Polling

For long‑running actions:

  1. Client requests an action.
http
POST /exports
Content-Type: application/json
{ "type": "orders", "from": "2026-01-01", "to": "2026-01-31" }
  1. Server responds:
http
202 Accepted
Location: /exports/abc123
{
  "id": "abc123",
  "status": "PENDING"
}
  1. Client polls:
http
GET /exports/abc123
  1. When completed:
json
{
  "id": "abc123",
  "status": "COMPLETED",
  "download_url": "https://files.example.com/exports/abc123.csv"
}

Internally, the Export service might:

From the client’s perspective, everything is a simple REST API that acknowledges that work is happening in the background.

Pattern 2: Event‑Driven Updates with Webhooks or WebSockets

In some cases, instead of polling, you push updates:

These fit naturally into distributed architectures, because services already communicate via events.

Example webhook payload:

http
POST https://client-app.example.com/webhooks/orders
{
  "event": "order.confirmed",
  "order_id": 123,
  "user_id": "user-123"
}

Practical Tips for Beginners

Distributed APIs can feel overwhelming. To approach them step by step:

  1. Start with a monolith
    Implement a clean REST API in one application. Separate layers clearly (routing, services, repositories). This makes it easier to split later.
  2. Introduce internal services when needed
    For example, split out an auth-service while keeping API gateway simple. Use consistent request / response patterns.
  3. Add a small API gateway or reverse proxy
    Use Nginx, Traefik, or a simple FastAPI gateway that routes requests to services. Add authentication and request logging at the gateway.
  4. Use IDs and correlation IDs everywhere
    • Stable resource IDs, like user_id and order_id
    • Correlation IDs for requests
  5. Design for retries and idempotency
    Any call that might be retried should be safe to repeat.
  6. Accept eventual consistency
    Document which parts of the system may be slightly out‑of‑date and for how long. Design your UX and APIs to handle that.
  7. Keep contracts explicit and documented
    Use OpenAPI for each service. Over time you can explore contract testing and stronger schema validation between services.

Summary

Distributed APIs are about making multiple services look like a coherent system:

As you build more complex backends, these distributed API patterns will help you evolve from a single app to a robust, multi‑service architecture.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!