Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 16

Defining Microservice Responsibilities and APIs

Recommended resources

Welcome back! In our last session, we took our first major architectural step by decomposing a monolithic e-commerce application into a set of candidate microservices based on business capabilities. We identified services like User, Catalog, Inventory, Ordering, and Payment.

Today, we will zoom in on one of these candidates—the Order Service—and give it a formal blueprint. The goal is to define its specific responsibilities and its public API contract. This contract is the most critical piece of a microservice; it's the formal agreement that dictates how the outside world can interact with it, enabling services to be developed, deployed, and scaled independently.

Step 1: Defining the Order Service's Responsibilities

Before designing the API, we must be crystal clear about what the Order Service is responsible for. Its core business capability is managing the entire lifecycle of a customer's order. A great way to visualize this lifecycle is with a state machine diagram.

Order State Machine Diagram
This diagram shows the possible states of an order and the transitions between them. This state machine is the foundation for our service's responsibilities.

Looking at this diagram, we can deduce the core responsibilities of the Order Service:

  • Order Creation: It must be able to create a new order for a user. This order starts in a Creating or Created state.
  • Order Modification: It should allow adding or removing items while the order is in a mutable state (e.g., Created).
  • Order Finalization: It needs a mechanism to lock the order and move it to a state like Awaiting Payment.
  • State Management: It is the single source of truth for the order's status. It must process state transitions, such as moving from Paid to Processing or Shipped. This might be triggered by internal logic or by receiving events from other services (e.g., a "Payment Successful" event from the Payment Service).
  • Order Retrieval: It must provide ways for a user to view their current and past orders.
  • Cancellation: It needs to handle requests to cancel an order, but only when the order is in a state where cancellation is permissible (e.g., not after it has been shipped).

Critically, as we established in the last lesson, the Order Service owns its data. It has exclusive control over the Orders and OrderItems tables. No other service can access this data directly; they must go through the API contract we are about to define. This principle of private data is fundamental to creating a loosely coupled system.


Step 2: The API as a Formal, Enforceable Contract

With clear responsibilities, we can design the API. In a microservices architecture, the API definition isn't just documentation; it's a formal, enforceable contract. Adopting a "contract-first" approach, where the API is designed and agreed upon before implementation begins, is a production best practice. It allows frontend teams, mobile teams, and other microservice teams to develop in parallel against a stable, well-defined interface.

To understand how seriously this is taken in industry, let's see how a high-scale company manages its API definitions.

Design Microservice Architectures the Right Way

This video from InfoQ explains how a contract-first approach is implemented in a real-world production environment at the company formerly known as Gilt.

Watch the segment on "API-first" development. Notice their practice of storing API definitions in a dedicated Git repository and using automated checks (linters) to enforce consistency. This elevates the API from a simple implementation detail to a core, version-controlled asset of the company.

This discipline ensures that the entire system remains coherent and that services don't accidentally break their contracts with each other.


Step 3: From Responsibilities to a Concrete Contract

Now we'll translate the Order Service's responsibilities into a concrete API contract. Since you're familiar with REST from your work with Django and FastAPI, we'll start by sketching out a conceptual RESTful API. The principles of good API design are universal, and this will serve as a perfect bridge to the gRPC syntax we'll use for the final contract.

How to Design APIs Like a Senior Engineer (REST, GraphQL, Auth, Security)

This video covers the fundamentals of designing a clean, intuitive RESTful API.

Watch the following sections to see how business requirements are mapped to an API structure: Resource Modeling: How to use nouns for resources, design URLs, and handle filtering and pagination. HTTP Methods: Using GET, POST, PUT, PATCH, and DELETE for CRUD operations. Status Codes and Error Handling: Communicating success, client errors, and server errors. Best Practices: A summary of key rules like using plural nouns and versioning.

Applying these principles, our conceptual REST API for the Order Service might look like this:

  • Create a new order: POST /v1/orders
  • Get a specific order: GET /v1/orders/{order_id}
  • List a user's orders: GET /v1/users/{user_id}/orders (with query params for pagination)
  • Add an item to an order: POST /v1/orders/{order_id}/items
  • Cancel an order: POST /v1/orders/{order_id}/cancel (Note: We use POST for actions that change state but aren't creating a new resource in the traditional sense).

This gives us a clear idea of the operations and resources. Now, let's formalize this using the native language of gRPC: Protocol Buffers.


Step 4: Defining the gRPC Contract with Protocol Buffers

In gRPC, the contract is defined in a .proto file. This file is language-agnostic, strongly typed, and serves as the source for generating both client and server code. This approach eliminates ambiguity and provides many benefits over traditional JSON/HTTP APIs.

Defining an API with Protocol Buffers

This Real Python article provides an excellent example of a .proto file and explains its components line by line.

Read the section starting from "Now you can define this API more formally..." down to the end of the "Why RPC and Protocol Buffers?" section. This will walk you through the syntax of a .proto file and explain the benefits of this approach, such as self-documentation, validation, and performance.

With that context, here is the official orders.proto contract for our Order Service. This file is the single source of truth for its public interface.

syntax = "proto3";

package orders.v1;

// We import a standard protobuf type for handling timestamps.
import "google/protobuf/timestamp.proto";

// Represents the current status of an order, mapping directly to our state machine.
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_CREATED = 1;     // Order created, items can be added/removed.
  ORDER_STATUS_AWAITING_PAYMENT = 2; // Order finalized, waiting for payment confirmation.
  ORDER_STATUS_PAID = 3;        // Payment received.
  ORDER_STATUS_PROCESSING = 4;  // Order is being prepared for shipment.
  ORDER_STATUS_SHIPPED = 5;     // Order has been shipped.
  ORDER_STATUS_DELIVERED = 6;   // Order has been delivered.
  ORDER_STATUS_CANCELLED = 7;   // Order was cancelled.
}

// Represents a single item within an order.
// We store a price snapshot to protect against future price changes.
message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price_per_item = 3; // Price at the time of order
}

// The main data structure for an order.
message Order {
  string order_id = 1;
  string user_id = 2;
  repeated OrderItem items = 3; // 'repeated' means it's a list of OrderItems.
  double total_amount = 4;
  OrderStatus status = 5;
  google.protobuf.Timestamp created_at = 6;
  google.protobuf.Timestamp updated_at = 7;
}

// --- RPC Request/Response Messages ---

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2; // Allows creating an order with initial items.
}

message GetOrderRequest {
  string order_id = 1;
}

message ListOrdersRequest {
  string user_id = 1;
  int32 page_size = 2; // How many orders to return.
  string page_token = 3; // Token from the previous response to get the next page.
}

message ListOrdersResponse {
  repeated Order orders = 1;
  string next_page_token = 2; // Token to use in the next request for pagination.
}

message CancelOrderRequest {
  string order_id = 1;
}

// The public API for the Order Service
service OrderService {
  // Creates a new order.
  rpc CreateOrder(CreateOrderRequest) returns (Order);

  // Retrieves a specific order by its ID.
  rpc GetOrder(GetOrderRequest) returns (Order);

  // Lists all orders for a given user with pagination.
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  
  // Cancels an order if it's in a cancellable state.
  rpc CancelOrder(CancelOrderRequest) returns (Order);
}

Analysis of the .proto file:

  • package orders.v1;: We are versioning our API right in the package name. This is a crucial practice for managing schema evolution without breaking existing clients.
  • enum OrderStatus: This directly implements our state machine, providing a clear, constrained set of possible statuses.
  • message: Each message defines a data structure with typed fields. Notice OrderItem stores price_per_item. This is important because the price of a product in the Catalog service might change later, but the order must reflect the price at the time of purchase.
  • service OrderService: This block defines the service itself.
  • rpc: Each rpc (Remote Procedure Call) defines an operation. It's like a function signature, specifying the method name (CreateOrder), the input message (CreateOrderRequest), and the output message (Order). This is the gRPC equivalent of our conceptual REST endpoints.

You may notice we omitted AddItemRequest. In a real-world scenario, you'd likely have it. For this example, we've simplified it to focus on the core lifecycle operations. The principles, however, remain the same.


Conclusion

In this lesson, we translated a high-level business capability into a formal, machine-readable API contract. This process of defining responsibilities and codifying them in a .proto file is the foundational act of building a microservice.

Key Takeaways:

  • A service's responsibilities are derived directly from its business capability, often visualized with tools like state machine diagrams.
  • The API is a formal contract that enables independent, parallel development and is a core, version-controlled asset.
  • Contract-first design, where the .proto file is created and agreed upon before implementation, is a production best practice.
  • Protocol Buffers provide a strongly-typed, language-agnostic way to define gRPC services, RPC methods, and the data messages they exchange.

Preview of the Next Lesson:

We have now defined what our Order Service does. But an order process involves multiple services. CreateOrder needs to check inventory and process a payment. How should these services talk to each other? In the next lesson, we will explore the critical patterns for inter-service communication, contrasting synchronous methods like the RPCs we just defined with asynchronous, event-driven patterns. We'll discuss the trade-offs and see when to use each approach to build a resilient and scalable system.