Capstone — Building a Small gRPC Service

gRPC — Capstone: Building a Small gRPC Service
gRPC
Chapter 9 · Capstone: Building a Small gRPC Service

🏁 Capstone: Building a Small gRPC Service

This capstone builds a small, real two-service system: UserService and OrderService, where OrderService calls UserService internally — combining schema design, streaming, interceptors, and auth from every prior chapter.

The System: UserService + OrderService

Requirements: an end user (with a bearer token) calls OrderService.CreateOrder; OrderService needs to look up the caller's details from UserService — a genuine internal service-to-service call, secured with mTLS; a client can then subscribe to live order status updates via streaming; every call on both services is logged.

Step 1: Schema Design (Chapter 2)

// user.proto service UserService { rpc GetUser(GetUserRequest) returns (User); } message GetUserRequest { int32 id = 1; } message User { int32 id = 1; string name = 2; } // order.proto service OrderService { rpc CreateOrder(CreateOrderRequest) returns (Order); rpc WatchOrderStatus(WatchOrderStatusRequest) returns (stream OrderStatusUpdate); } message CreateOrderRequest { int32 user_id = 1; repeated string item_names = 2; } message Order { int32 id = 1; string customer_name = 2; string status = 3; }

Step 2: The Service-to-Service Call (Chapters 1, 3, 7)

Creating an order needs the customer's name — OrderService fetches it by calling UserService directly, exactly the internal service-to-service pattern Chapter 1 named as gRPC's original purpose, secured with the mutual TLS Chapter 7 covered.

async function createOrder(call, callback) { const { user_id, item_names } = call.request; // OrderService calling UserService internally, over mTLS userServiceClient.getUser({ id: user_id }, (err, user) => { if (err) return callback(err); const order = database.createOrder({ customer_name: user.name, item_names }); callback(null, order); }); }

Step 3: Streaming Order Status (Chapter 4)

A server-streaming RPC lets a client subscribe to live updates as an order moves through processing.

function watchOrderStatus(call) { const unsubscribe = orderEvents.on(call.request.order_id, (status) => { call.write({ status }); }); call.on('cancelled', unsubscribe); }

Step 4: Interceptors Tying It Together (Chapter 6)

A shared logging interceptor applies to both services; an auth interceptor on OrderService validates the end user's bearer token before createOrder ever runs.

orderServer.use(loggingInterceptor); // registered first — logs every attempt orderServer.use(authInterceptor); // validates the bearer token userServer.use(loggingInterceptor); userServer.use(mtlsCheckInterceptor); // only accepts calls with a valid client cert

Step 5: Putting It All Together

  1. A client (holding a bearer token) calls OrderService.CreateOrder.
  2. OrderService's logging interceptor records the attempt; its auth interceptor validates the token, rejecting with UNAUTHENTICATED if invalid.
  3. The createOrder handler calls UserService.GetUser internally, over an mTLS-secured channel — UserService's own interceptors verify the client certificate before responding.
  4. The order is created and returned to the original client.
  5. The client calls WatchOrderStatus to receive live status updates as the order is processed.
Course ConceptWhere It's Used in This App
Service definitions & messages (Ch.2)user.proto / order.proto
Service-to-service calls (Ch.1, 3)OrderService calling UserService's GetUser
Server streaming (Ch.4)WatchOrderStatus
Interceptors (Ch.6)Shared logging + auth on OrderService, mTLS check on UserService
Bearer tokens & mTLS (Ch.7)End-user auth on OrderService; service identity on the OrderService→UserService call

💻 Coding Challenges

Challenge 1: Add Error Handling to the Internal Call

Extend Step 2's createOrder handler so that if UserService.GetUser returns NOT_FOUND, the whole CreateOrder call fails with a clear, matching error rather than a generic one.

Goal: Practice propagating a meaningful gRPC status code (Chapter 5) across a service-to-service call.

→ Solution

Challenge 2: Design a Client-Streaming Addition

Design a new client-streaming RPC, ImportOrders, that lets a client submit many orders in one call and receive a single summary response with the total count created.

Goal: Practice applying Chapter 4's client-streaming mode to a new scenario within this capstone's schema.

→ Solution

Challenge 3: Justify the Interceptor Order

Explain why Step 4 registers the logging interceptor before the auth interceptor on OrderService, referencing Chapter 6's material.

Goal: Practice applying the interceptor-ordering lesson to this capstone's specific setup.

→ Solution

⚠️ Gotcha: Deadlines Need to Be Threaded Through the Internal Call Too

Chapter 5 established that deadlines propagate across a call chain — but only if the code actually threads them through. If createOrder's call to UserService.GetUser doesn't derive its own deadline from the incoming client call's remaining time budget, the two calls end up with independent timeouts: the original client's deadline could expire while OrderService is still waiting on a UserService call that has no idea a time budget even exists. Every internal call made from within a handler needs to explicitly forward (or derive from) the deadline it received, not start a fresh, unrelated one — exactly the kind of detail that's easy to get right in a single-service example and easy to silently miss the moment a real call chain like this one exists.

🎉 Course Complete

That's the full gRPC course — from the RPC model and HTTP/2 transport through Protocol Buffers, code generation, all four streaming modes, error handling and metadata, interceptors, authentication and security, microservices/load balancing, and finally a real two-service system combining all of it. Together with API Types & Design and API Testing & Tooling, this completes the site's "APIs & Integration" area.