Create your own
Lesson illustration

Designing API Contracts and Communication Patterns

Hello! Welcome back to our final module, System Design & Production Readiness.

In our last lesson, we took the first and most critical step in system design: decomposing a complex system into microservices using Domain-Driven Design. We saw how identifying bounded contexts—like Trip Management, Driver Management, and Payment for a ride-sharing app—gives us a set of cohesive, loosely coupled service candidates.

Now that we have our services, the next logical question an interviewer will ask is, "How do they talk to each other?" This brings us to today's topic.

Today, you will learn to design API contracts and choose appropriate communication patterns for key inter-service interactions. This is where we define the "seams" between our services, specifying not just what they do, but how they interact in a reliable, scalable, and evolvable way. This skill is fundamental to demonstrating senior-level architectural thinking.

1. The API as a Contract

An API (Application Programming Interface) is more than just code; it's a formal contract between a service and its consumers. This contract defines the available operations, the structure of requests and responses, and the expected behavior.

In a microservices architecture, we typically deal with two distinct types of APIs:

  • Public APIs: These are exposed to external clients like web browsers or mobile apps. They must prioritize broad compatibility, ease of use, and strong security.
  • Backend APIs: These are used for internal, service-to-service communication. Here, raw performance, efficiency, and low latency are often more critical than universal compatibility.

API design - Azure Architecture Center

The Microsoft Azure Architecture Center provides a concise overview of this distinction and the key factors to consider when designing APIs in a microservices context.

Please read the introduction and the 'Considerations' section. Focus on the distinction between public and back-end APIs and the trade-offs mentioned, such as REST vs. RPC, efficiency, and serialization formats.

The article mentions several communication styles, but for a synchronous, request-response model, three technologies dominate the conversation in system design interviews: REST, GraphQL, and gRPC. Let's dive into each one.

2. Choosing Your Communication Protocol

When designing inter-service communication, you're not just picking a technology; you're making a series of trade-offs. The choice of protocol impacts performance, developer experience, and how your system can evolve.

The following video provides an excellent, interview-focused overview of REST, GraphQL, and gRPC. It explains what they are, when to use them, and what to watch out for.

API Design in System Design Interviews w/ Meta Staff Engineer

This video from 'Hello Interview' is presented by a former Meta staff engineer and is tailored specifically for system design interviews. It will serve as our foundation for understanding the three main synchronous API protocols.

Watch the following segments: Introduction to Protocols (02:34 - 03:59): Get an overview of REST, GraphQL, and RPC and their primary use cases. Deep Dive on REST (03:59 - 12:13): This is critical. Pay close attention to resources (plural nouns), HTTP verbs, idempotency, path vs. query parameters, and response codes. This is your default choice for many APIs. Introduction to GraphQL (12:13 - 15:43): Understand the problem GraphQL solves (over/under-fetching) and how its query language works. Introduction to RPC/gRPC (17:20 - 19:36): Focus on why gRPC is so fast and why it's a great choice for internal service-to-service communication.

Let's consolidate and expand on what you've just seen.

2.1. REST: The Universal Standard

REST is the de facto standard for public-facing APIs. Its principles are built on the foundation of the web itself: HTTP.

  • Style: Architectural Style (not a strict protocol).
  • Transport: HTTP/1.1 (typically).
  • Payload: JSON (human-readable text).
  • Contract: Defined using OpenAPI (formerly Swagger).

Key Principles:

  • Resource-Oriented: Everything is a resource, identified by a URL (e.g., /drivers, /trips/{tripId}). Resources should be nouns, not verbs.
  • Standard Verbs: Use HTTP methods to express actions:
    • GET: Retrieve a resource. Idempotent.
    • POST: Create a new resource. Not idempotent.
    • PUT: Replace an existing resource. Idempotent.
    • PATCH: Partially update a resource. Not typically idempotent.
    • DELETE: Delete a resource. Idempotent.
  • Statelessness: Each request from a client contains all the information needed to process it.

When to use REST:

  • Public-facing APIs: Its ubiquity means any developer or client can easily consume it.
  • Simple CRUD-oriented services: When you're primarily creating, reading, updating, and deleting resources.
  • When you need to support browser clients directly.

2.2. gRPC: High-Performance Internal Communication

gRPC is a modern framework for Remote Procedure Calls (RPC) developed by Google. It's designed for speed and efficiency.

Microservices Communication Patterns and Centralized Message Contracts
This diagram illustrates a common architecture. Synchronous, high-performance communication between `OrderService` and `PaymentService` is handled by gRPC. Asynchronous communication to other services uses a message bus. The contracts for all are centrally defined.
  • Style: Remote Procedure Call (RPC). You call methods on a remote service as if they were local.
  • Transport: HTTP/2, enabling multiplexing and header compression.
  • Payload: Protocol Buffers (Protobuf), a compact, efficient binary format.
  • Contract: Defined in .proto files, which are strongly typed and can be used to auto-generate client and server code in multiple languages.

When to use gRPC:

  • Internal service-to-service communication: This is its primary sweet spot. The performance gains are significant in a microservices environment.
  • Low-latency, high-throughput systems: Financial trading, real-time analytics, etc.
  • Polyglot environments: When services are written in different languages, the .proto contract ensures they can communicate seamlessly.

2.3. GraphQL: Flexible and Client-Driven

GraphQL is a query language for APIs, developed by Facebook to address the needs of complex UIs and mobile applications.

  • Style: Query Language.
  • Transport: HTTP/1.1 (typically).
  • Payload: JSON.
  • Contract: Defined using a GraphQL Schema Definition Language (SDL).

Key Differentiator: Instead of multiple endpoints that return fixed data structures (like REST), GraphQL exposes a single endpoint (e.g., /graphql). The client sends a query specifying exactly the data fields it needs, and the server returns a JSON object with precisely that data—no more, no less.

When to use GraphQL:

  • Mobile applications: Helps minimize network usage by fetching only necessary data.
  • Complex UIs with multiple components: A single GraphQL query can aggregate data from multiple backend sources, replacing many REST calls.
  • When your API consumers evolve rapidly: Frontend teams can change their data requirements without needing backend API changes.
Test your understanding!

You are designing a system with three services: Product Catalog, Shopping Cart, and Order Service. A mobile client needs to display a user's shopping cart, which includes product names, images, and prices.

Which communication pattern is most appropriate for the interaction between the mobile client and the backend? Which pattern is best for the internal communication when the Shopping Cart service needs product details from the Product Catalog service? Justify your choices.

Show answer
  • Mobile Client to Backend: GraphQL is an excellent choice here. The mobile client can send a single query to fetch all the necessary data for the shopping cart view (cart items, product details, prices). This avoids the "N+1" problem of fetching the cart and then making separate REST calls for each product's details. It also saves bandwidth, which is critical on mobile networks. REST could also work, but would likely require a dedicated Backend-for-Frontend (BFF) service to aggregate the data efficiently.

  • Internal (Shopping Cart to Product Catalog): gRPC is the ideal choice for this internal, service-to-service communication. The Shopping Cart service needs to fetch product information from the Product Catalog. Using gRPC provides a low-latency, high-performance connection. The strongly-typed contract defined in a .proto file ensures reliability and makes it easy to maintain the API between the two teams developing these services. REST could be used, but you'd be sacrificing performance for no real gain in this internal context.

3. API Design Best Practices and Pitfalls

Defining a good API contract goes beyond just choosing a protocol. It's about creating an interface that is clear, robust, and can evolve gracefully. This is where you can really demonstrate seniority in an interview.

Top 10 Rest API Design Pitfalls by Victor Rentea @ Spring I/O 2025

Let's now watch some clips from a fantastic talk by Victor Rentea on common API design pitfalls. This will give you a deeper, more practical understanding of what makes a 'production-ready' API.

Please watch these specific segments, as they cover crucial design considerations: Backward Compatibility & Versioning (01:17 - 07:41): Understand what a 'breaking change' is and why you must avoid it. Pay attention to the discussion around API versioning (V1, V2). DTOs vs. Domain Models (23:06 - 29:00): This is a critical point. He explains why you should never expose your internal domain/JPA entities directly in your API and the importance of using Data Transfer Objects (DTOs). Action-Oriented APIs (38:32 - 46:32): This is an advanced concept. Understand the limitations of pure CRUD and the value of creating action-oriented endpoints (e.g., POST /orders/{id}/cancel) that model business intent. Error Handling (48:27 - 50:20): Learn the best practice of returning all validation errors at once, rather than one by one.

Key Design Principles to Remember:

  1. Separate Contract from Implementation (DTOs): Always use Data Transfer Objects (DTOs) for your API requests and responses. Never expose your internal JPA entities or domain models. This allows you to evolve your internal logic and database schema without breaking the public API contract.
  2. Plan for Evolution (Versioning): Breaking changes are inevitable. Plan for them by versioning your API (e.g., /api/v1/users, /api/v2/users). This allows you to introduce new versions while giving clients time to migrate from older ones.
  3. Handle Partial Data (Pagination): For any endpoint that can return a large list of items (GET /orders), you must implement pagination. The two main strategies are offset-based (?page=2&size=25) and cursor-based (?cursor=...&limit=25). Cursor-based is more robust for data sets that change frequently.
  4. Design for User Intent (Beyond CRUD): Don't just think in terms of Create, Read, Update, Delete. Model specific business actions. Instead of a generic PUT /orders/{id} that does everything, consider more explicit endpoints like POST /orders/{id}/submit, POST /orders/{id}/cancel, or PUT /orders/{id}/shipping-address. This makes your API more meaningful and your server-side logic cleaner.
  5. Be Explicit about Authentication: For endpoints that require a user to be logged in (e.g., creating a tweet), the user's identity should come from a secure token in the request header (like a JWT), not from a userId field in the request body. This prevents one user from performing actions on behalf of another.

4. The Hybrid Approach: Best of Both Worlds

In many modern systems, you don't have to choose just one protocol. A highly effective and common pattern is to use different protocols for different purposes.

The API Gateway Pattern:

  • External/Public API: Use REST or GraphQL at the edge. An API Gateway receives requests from clients (browsers, mobile apps).
  • Internal/Backend API: Use gRPC for communication between the API Gateway and the internal microservices, and for communication between the microservices themselves.
Synchronous vs. Asynchronous Microservice Communication Patterns
This image shows two flows. The top is a fully synchronous HTTP flow. The bottom illustrates a hybrid approach: an external HTTP request kicks off an internal flow that uses asynchronous messaging (Kafka). A similar hybrid pattern exists for synchronous communication, where an external REST call could trigger internal gRPC calls.

This hybrid approach gives you the best of both worlds: the universal accessibility of REST for external clients and the high performance of gRPC for your internal network.

REST or gRPC? A Guide to Efficient API Design

This article provides a great summary comparing REST and gRPC and explicitly describes the hybrid gateway pattern. It's a good way to solidify your understanding of when to use which.

Read the following sections: Choosing Your Champion: This gives clear use cases for when REST or gRPC is the better choice. Best of Both Worlds with REST and gRPC: Focus on the 'Gateway Pattern: REST Outside, gRPC Inside'. Making the Right Choice: A Decision Framework: Review the decision matrix. This is a great mental model for interviews.

5. Conclusion and Interview Preparation

Today, you've learned how to bridge the services you identified in the last lesson by defining their API contracts and choosing the right communication patterns.

Key Takeaways:

  • APIs are Contracts: Distinguish between public-facing APIs (favoring REST/GraphQL for compatibility) and internal APIs (favoring gRPC for performance).
  • Know Your Protocols: Understand the core trade-offs between REST (universal, text-based), gRPC (performant, binary, RPC), and GraphQL (flexible, client-driven).
  • Embrace the Hybrid Model: The pattern of using REST/GraphQL at the API Gateway and gRPC for internal services is a powerful and common solution in modern architectures.
  • Design for Evolution: Use DTOs, versioning, and action-oriented endpoints to create robust and maintainable APIs.
Senior Engineer Interview Question

"Continuing with the ride-sharing platform we discussed, let's focus on the Trip Service, Driver Service, and Payment Service.

  1. Design the REST API contract for creating a new trip. Specify the endpoint, HTTP method, a sample request body, and expected success/error responses.
  2. When a trip is completed, the Trip Service needs to tell the Payment Service to process the payment. Which communication protocol would you choose for this interaction and why?
  3. The mobile app needs a screen showing a user's trip history, including the route map, driver's name, and final fare for each trip. This data comes from the Trip Service, Driver Service, and Payment Service. How would you design the API to efficiently provide this data to the mobile app?"
Show detailed answer

"This is a great question that touches on several key design trade-offs. Here's how I would approach it.

1. REST API for Creating a Trip

I would choose REST for this public-facing API because it will be called by our mobile clients, and REST's ubiquity makes it easy to work with.

  • Endpoint: POST /v1/trips

  • Method: POST is appropriate because we are creating a new resource, and this operation is not idempotent. Sending the same request twice should create two separate trip requests.

  • Sample Request Body (DTO):

    {
      "pickup_location": {
        "latitude": 34.0522,
        "longitude": -118.2437
      },
      "destination_location": {
        "latitude": 34.0112,
        "longitude": -118.4927
      },
      "vehicle_type": "STANDARD"
    }
    

    Note that the rider_id is not in the body. It would be extracted from the authenticated user's JWT in the Authorization header.

  • Responses:

    • Success (202 Accepted): Creating a trip involves finding a driver, which is an asynchronous process. So, I would immediately return a 202 Accepted status with a Location header pointing to the newly created trip resource (e.g., Location: /v1/trips/trip-uuid-123). The response body could contain the initial state of the trip. The client can then poll this resource or listen on a WebSocket for updates (e.g., DRIVER_ASSIGNED).
    • Client Error (400 Bad Request): If the request is malformed (e.g., missing location), I'd return a 400 with a detailed error body listing all validation failures.
    • Authentication Error (401 Unauthorized): If the JWT is missing or invalid.

2. Internal Communication: Trip Service to Payment Service

For this internal, service-to-service communication, I would choose gRPC.

  • Why gRPC?
    • Performance: This is a high-volume internal call. gRPC's use of HTTP/2 and binary Protocol Buffers offers significantly lower latency and smaller payload sizes than a JSON/HTTP-based REST call.
    • Strongly-Typed Contract: The interaction can be rigidly defined in a .proto file (e.g., a ChargeUserForTrip method). This generates client and server code, reducing boilerplate and catching integration errors at compile time, which is crucial for reliability between two critical services.
    • Efficiency: In a microservices environment with potentially thousands of these calls per minute, the efficiency gains from gRPC are substantial.

The Trip Service would use a gRPC client to call a method like paymentService.chargeUserForTrip({ tripId: "...", amount: "..." }).

3. API for Trip History Screen

For this requirement, I would strongly advocate for GraphQL.

  • Why GraphQL?
    • Avoids Over-fetching and Under-fetching: The trip history screen needs a specific combination of data from three different services (Trip, Driver, Payment). A traditional REST approach would require either:
      a) The mobile app making three separate calls (one to each service's REST endpoint), which is inefficient and slow.
      b) A dedicated BFF (Backend-for-Frontend) REST endpoint like GET /v1/trip-history-details that aggregates the data. This works, but you end up creating many such custom endpoints.
    • Client-driven Flexibility: GraphQL provides a single endpoint where the mobile client can specify exactly the fields it needs:
      query GetTripHistory {
        tripHistory {
          id
          routeMapUrl
          finalFare {
            amount
            currency
          }
          driver {
            name
          }
        }
      }
      
    • Single Round Trip: The client gets all the data it needs in a single network request, which is ideal for mobile performance. The GraphQL server would be responsible for internally calling the Trip, Driver, and Payment services (likely via gRPC) to resolve this data. This approach puts the power in the hands of the client developers, allowing them to evolve the UI without requiring new backend deployments."

Fantastic work today. We've defined how our services will communicate. The next piece of the puzzle is how they manage their data. In our next lesson, we will select and justify data management strategies (e.g., CQRS, event sourcing) for different services within the system, which will lead us into a discussion of eventual consistency and distributed transactions.

Can't find a good explanation? Sign up and we'll make it for you

Sign up