TechCorp now has its two main channels: REST/JSON for synchronous calls (03-01) and RabbitMQ for events (03-02). With those you can build the whole system, and in fact that is what we will do in module 4. But it is worth knowing two alternatives that solve specific problems REST/JSON solves poorly: gRPC, for high-volume internal calls with a typed, binary contract, and GraphQL, so that a front end can request exactly the data it needs from several services in a single query. They are neither fads nor replacements for REST: they are tools with a clear niche, and knowing what it is prevents both ignoring them and using them where they do not belong.
In this lesson we will look at which limitations of REST/JSON motivate each alternative; gRPC with Protocol Buffers (a real .proto for the Inventory service), HTTP/2, the four call types, and a minimal server and client in Node.js with @grpc/grpc-js explained line by line; GraphQL with a schema of orders and products, resolvers, the N+1 problem and DataLoader; a comparison table of REST, gRPC, GraphQL and messaging; and TechCorp's decision on where each one fits. The gateway and the BFF (where GraphQL would shine) belong to 03-04, and versioning of .proto files and GraphQL schemas, to 03-06.
Contents
- Limitations of REST/JSON that motivate alternatives
- gRPC: what it is and how it works
- Protocol Buffers: Inventory's
.protocontract - gRPC server and client in Node.js
- Errors in gRPC and when to use it
- GraphQL: schema, queries and mutations
- Resolvers in Node.js
- The N+1 problem and DataLoader
- When to use GraphQL and its risks
- Comparison: REST, gRPC, GraphQL and messaging
- TechCorp's decision
- Limitations of REST/JSON that motivate alternatives
REST over JSON is the de facto standard because it is simple, universal and readable. Precisely because of that it has three shortcomings that become noticeable as a system grows:
| Limitation | What it consists of | Where TechCorp would suffer it |
|---|---|---|
| Verbosity and serialization cost | JSON is text: every field repeats its name, numbers travel as strings of digits, and parsing it costs CPU. HTTP/1.1 opens connections and sends full headers on every request. | Orders → Inventory if it became synchronous: thousands of calls per minute at the peak, each with the same JSON of lines. |
| Lack of a strong contract | OpenAPI describes the API, but it is a document separate from the code: nothing prevents the server from returning price as a string one day. Clients are hand-written or optionally generated. |
A change in Catalog that breaks Orders' productTranslator is discovered in production or, with luck, in 04-05. |
| Over-fetching and under-fetching | An endpoint returns a fixed shape. If the front end needs less, there is excess (over-fetching); if it needs data from two resources, it makes two requests (under-fetching). | The mobile app's "order detail" screen needs the order, the products with their images and the payment status: three calls, or a bespoke endpoint per screen. |
gRPC attacks the first two (compact binary, HTTP/2 and a contract generated from a .proto). GraphQL attacks the third (the client declares the exact shape it wants). Neither attacks all three, and neither solves what messaging solves (temporal coupling).
- gRPC: what it is and how it works
gRPC is a remote procedure call (RPC) framework created by Google. Its ingredients:
- Contract first. You write a
.protofile that defines the services, their methods and the messages they exchange. From it, server and client code is generated in whatever language (Node.js, Go, Java...). The contract is not documentation: it is the source. - Protocol Buffers (protobuf) as the serialization format: binary, compact (an
int32takes 1-5 bytes; in JSON,"quantity": 2takes 13), and with a schema (the receiver knows the type of every field without guessing). - HTTP/2 as the transport: a single TCP connection multiplexes many calls in parallel, headers are compressed, and there is native streaming in both directions.
- Four call types:
| Type | Client sends | Server returns | Example at TechCorp |
|---|---|---|---|
| Unary | 1 message | 1 message | ReserveStock(ReservationRequest) → Reservation |
| Server streaming | 1 message | Stream of N messages | WatchAvailability(productId) → stream Availability (the client receives every stock change) |
| Client streaming | Stream of N messages | 1 message | ImportStock(stream Movement) → Summary (bulk load from the warehouse) |
| Bidirectional | Stream | Stream | Real-time support chat (out of TechCorp's scope) |
The right mental model: gRPC is like calling a function that lives in another process, with types checked at compile time (or at load time, in Node.js). REST is like manipulating documents. That is why gRPC fits internal service-to-service calls and REST fits public APIs.
- Protocol Buffers: Inventory's
.proto contract
.proto contractIn 03-01 we designed POST /reservations as Inventory's REST contract "in case the Orders↔Inventory relationship becomes synchronous". This is the same contract in gRPC:
// inventory.proto
syntax = "proto3";
package techcorp.inventory.v1;
// The service: each rpc is a remote method
service InventoryService {
// Unary: reserve stock for an order (equivalent to POST /reservations)
rpc ReserveStock (ReservationRequest) returns (Reservation);
// Unary: check availability of several products (batch, like GET /products?ids=)
rpc CheckAvailability (AvailabilityRequest) returns (AvailabilityResponse);
// Server streaming: receive availability changes for a product
rpc WatchAvailability (WatchRequest) returns (stream Availability);
}
// The messages: each field has a type, a name and a NUMBER. The number is what travels over the wire;
// the name is only for the code. That is why a number is never reused (03-06).
message ReservationLine {
string product_id = 1; // "p-501"
int32 quantity = 2;
}
message ReservationRequest {
string order_id = 1; // "ord-88213"; also serves as the idempotency key
repeated ReservationLine lines = 2; // repeated = list
int32 expires_in_seconds = 3; // 900
}
enum ReservationStatus {
RESERVATION_STATUS_UNSPECIFIED = 0; // proto3 requires a default value of 0
ACTIVE = 1;
CONSUMED = 2;
RELEASED = 3;
}
message Reservation {
string id = 1; // "res-4471"
string order_id = 2;
ReservationStatus status = 3;
string expires_at = 4; // ISO 8601; there is a standard Timestamp type, but this is more didactic
repeated ReservationLine lines = 5;
}
message AvailabilityRequest {
repeated string product_ids = 1;
}
message Availability {
string product_id = 1;
int32 available_units = 2;
bool available = 3;
}
message AvailabilityResponse {
repeated Availability products = 1;
}
message WatchRequest {
string product_id = 1;
}How to read it:
syntax = "proto3"is the current version of the language.packageprovides a namespace (and, withv1, leaves room for versioning, the subject of 03-06).servicegroups therpcs. Eachrpchas an input message and an output message;streamin front of one of them turns it into a stream.messageis like astruct. Each field carries a type (string,int32,bool,repeated X, anothermessage,enum), a name insnake_case(the protobuf convention; the generated code converts it tocamelCasein JavaScript if asked to) and a field number unique within the message. That number is the field's identity in the binary: changing the name breaks nothing; changing the number does.- In proto3 all fields are optional and have a default value (
"",0,false, empty list). There is nonull: you have to decide how to represent "absent" (withoptional, available since protobuf 3.15, or with a wrapper message). enums must have a0value, which is the default; by convention it is named*_UNSPECIFIEDto tell it apart from a real value.
Notice that this .proto is the contract between Orders and Inventory: if Luis's team and the Inventory team (which in 02-01 we saw are the same team) agree on it, each generates its side and works in parallel.
- gRPC server and client in Node.js
In Node.js there are two ways to use a .proto: generate static code with protoc and the JavaScript plugin, or load it dynamically at runtime with @grpc/proto-loader. The second is the simplest for learning and the one we will use; in production TechCorp would also use dynamic loading unless it needs generated TypeScript types.
Server (inside inventory-service; only the gRPC part, the rest of the service belongs to module 4):
// grpc/inventoryServer.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// 1. Load and "compile" the .proto in memory.
// keepCase:false converts product_id → productId in the JS objects.
// longs/enums/defaults control how types are represented; these values are the usual ones.
const definition = protoLoader.loadSync(path.join(__dirname, 'inventory.proto'), {
keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
// 2. Turn the definition into usable gRPC objects; we navigate down to the package
const proto = grpc.loadPackageDefinition(definition).techcorp.inventory.v1;
// 3. Implementation of each rpc. The signature is always (call, callback) for unary calls.
// call.request is the input message already deserialized; callback(error, response) responds.
const implementation = {
ReserveStock: async (call, callback) => {
const { orderId, lines, expiresInSeconds } = call.request;
try {
// The real logic (transaction on stock/reservations, publishing stock.reserved) belongs to module 4
const reservation = await useCases.reserveStock({ orderId, lines, expiresInSeconds });
callback(null, {
id: reservation.id, orderId: reservation.orderId, status: 'ACTIVE',
expiresAt: reservation.expiresAt.toISOString(), lines: reservation.lines
});
} catch (err) {
// 4. Errors are communicated with gRPC codes, not with exceptions (section 5)
if (err.code === 'OUT_OF_STOCK') {
return callback({ code: grpc.status.FAILED_PRECONDITION, details: `OUT_OF_STOCK: ${err.message}` });
}
callback({ code: grpc.status.INTERNAL, details: 'Internal error' });
}
},
CheckAvailability: async (call, callback) => {
const { productIds } = call.request;
if (productIds.length === 0 || productIds.length > 100) {
return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'Between 1 and 100 ids' });
}
const rows = await stockRepository.availabilityOf(productIds);
callback(null, {
products: rows.map(r => ({ productId: r.productId, availableUnits: r.available, available: r.available > 0 }))
});
},
// 5. Server streaming: no callback; we write to 'call' as many times as needed and close with end()
WatchAvailability: (call) => {
const { productId } = call.request;
const unsubscribe = stockNotifier.subscribe(productId, (avail) => {
call.write({ productId, availableUnits: avail, available: avail > 0 });
});
call.on('cancelled', unsubscribe); // the client hung up: stop sending
}
};
// 6. Create the server, register the service with its implementation and listen.
// createInsecure() = no TLS; fine inside the cluster for learning. TLS/mTLS is covered in 07-02.
function startGrpc(port = 50051) {
const server = new grpc.Server();
server.addService(proto.InventoryService.service, implementation);
server.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) throw err;
console.log(`gRPC InventoryService listening on ${port}`);
});
return server;
}
module.exports = { startGrpc };Client (inside orders-service):
// grpc/inventoryClient.js
const path = require('node:path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// 1. Same .proto, same options: the contract is shared (in an npm package or a contracts repo)
const definition = protoLoader.loadSync(path.join(__dirname, 'inventory.proto'), {
keepCase: false, longs: String, enums: String, defaults: true, oneofs: true
});
const proto = grpc.loadPackageDefinition(definition).techcorp.inventory.v1;
// 2. A stub: an object with one method per rpc. The address is the stable service name (03-05).
const INVENTORY_GRPC = process.env.INVENTORY_GRPC ?? 'inventory-service:50051';
const stub = new proto.InventoryService(INVENTORY_GRPC, grpc.credentials.createInsecure());
// 3. Stubs use callbacks; we wrap them in promises to use them with async/await
function reserveStock(request, { requestId } = {}) {
return new Promise((resolve, reject) => {
// 4. Metadata = gRPC headers. We propagate X-Request-Id just like in REST.
const metadata = new grpc.Metadata();
if (requestId) metadata.set('x-request-id', requestId);
// 5. deadline = absolute timeout: if there is no response within 2 s, DEADLINE_EXCEEDED error
const deadline = new Date(Date.now() + 2000);
stub.ReserveStock(request, metadata, { deadline }, (err, response) => {
if (err) return reject(err); // err.code is a grpc.status; err.details the text
resolve(response);
});
});
}
module.exports = { reserveStock };And its use, with our usual order:
try {
const reservation = await reserveStock({
orderId: 'ord-88213',
lines: [{ productId: 'p-501', quantity: 1 }, { productId: 'p-777', quantity: 2 }],
expiresInSeconds: 900
}, { requestId: 'req-01J4ZK9X2M' });
console.log(reservation.id, reservation.status); // res-4471 ACTIVE
} catch (err) {
if (err.code === grpc.status.FAILED_PRECONDITION) { /* OUT_OF_STOCK → cancel order */ }
else if (err.code === grpc.status.DEADLINE_EXCEEDED) { /* Inventory not responding → 503 */ }
else throw err;
}The essentials: the .proto is the only shared file; server and client load it and get typed objects; the method and field names come from the contract, not from a hand-written URL; and the deadline is the equivalent of the AbortSignal.timeout from 03-01.
- Errors in gRPC and when to use it
gRPC does not use HTTP codes: it has its own, fewer and more precise. The ones TechCorp maps:
| gRPC code | REST equivalent | When |
|---|---|---|
OK (0) |
200 | Success |
INVALID_ARGUMENT (3) |
400 / 422 | Malformed request or invalid values |
NOT_FOUND (5) |
404 | Nonexistent resource |
ALREADY_EXISTS (6) |
409 | Already exists (duplicate reservation without idempotency) |
FAILED_PRECONDITION (9) |
409 | The state does not allow the operation: OUT_OF_STOCK, invalid transition |
PERMISSION_DENIED (7) / UNAUTHENTICATED (16) |
403 / 401 | Authorization / authentication |
RESOURCE_EXHAUSTED (8) |
429 | Limit exceeded |
DEADLINE_EXCEEDED (4) |
503/504 on the caller | Timeout |
UNAVAILABLE (14) |
503 | Server down or starting up; the client may retry |
INTERNAL (13) |
500 | Unhandled failure |
The business code (OUT_OF_STOCK) travels in details or, better, in a structured error message (google.rpc.Status with typed details); for TechCorp the prefix in details plus a mapping in the client is enough.
When to use gRPC:
- Internal service-to-service communication with high volume or a low-latency requirement: binary serialization and HTTP/2 become noticeable from thousands of calls per second upward.
- When you want a strong contract and generated code in several languages (teams with Go, Java and Node.js).
- Streaming (progress, real-time tracking).
When not to:
- Public APIs consumed by browsers: browsers do not speak native gRPC (gRPC-Web exists with a proxy, but it adds parts), and external developers expect REST.
- When readability matters more than performance: you cannot
curlit and read the response. - Small teams without a performance problem: it is complexity with no return.
For TechCorp: Orders → Inventory is the natural candidate if it ever becomes synchronous (partnership from 02-03, same team, frequent calls). Today it goes through events and will stay that way; the .proto is designed and ready.
- GraphQL: schema, queries and mutations
GraphQL is a query language for APIs and a runtime that executes them. The central idea: the server publishes a typed schema of everything that can be requested, and the client sends a query describing exactly the shape of the response it wants. A single endpoint (POST /graphql), a single round trip, not one field more or less.
Schema (SDL, Schema Definition Language) for an orders view that combines data from Orders and from Catalog:
# schema.graphql
type Product {
id: ID!
name: String!
price: Float!
currency: String!
imageUrl: String
available: Boolean!
}
type OrderLine {
productId: ID!
name: String! # frozen in the order (02-03)
quantity: Int!
unitPrice: Float!
product: Product # LIVE data from Catalog! (image, current availability)
}
enum OrderStatus { PENDING STOCK_RESERVED PAID CONFIRMED CANCELLED }
type Address { street: String!, postalCode: String!, city: String!, country: String! }
type Order {
id: ID!
status: OrderStatus!
customerId: ID!
lines: [OrderLine!]!
total: Float!
shippingAddress: Address!
createdAt: String!
}
type Query {
order(id: ID!): Order
customerOrders(customerId: ID!, limit: Int = 20, cursor: String): [Order!]!
products(ids: [ID!]!): [Product!]!
}
input LineInput { productId: ID!, quantity: Int! }
input AddressInput { street: String!, postalCode: String!, city: String!, country: String! }
type Mutation {
createOrder(customerId: ID!, lines: [LineInput!]!, shippingAddress: AddressInput!): Order!
}How to read it: type defines objects with typed fields (! = non-null; [X!]! = non-null list of non-null elements); Query holds the reads and Mutation the writes (both are just special types); input types are for arguments; enum as in protobuf. Notice OrderLine.product: a field that crosses services. It is what REST does not give you without a bespoke endpoint.
The mobile client's query for the order detail screen:
query OrderDetail($id: ID!) {
order(id: $id) {
id
status
total
lines {
name
quantity
unitPrice
product { imageUrl available }
}
}
}With {"id": "ord-88213"} as variables, the response has exactly that shape:
{
"data": {
"order": {
"id": "ord-88213",
"status": "CONFIRMED",
"total": 79.7,
"lines": [
{ "name": "BT X200 Headphones", "quantity": 1, "unitPrice": 59.9, "product": { "imageUrl": "https://cdn.techcorp.example/p-501.webp", "available": true } },
{ "name": "USB-C Cable 2 m", "quantity": 2, "unitPrice": 9.9, "product": { "imageUrl": "https://cdn.techcorp.example/p-777.webp", "available": true } }
]
}
}
}No customerId, no shippingAddress, no createdAt: they were not requested. And a mutation:
mutation {
createOrder(
customerId: "c-1024",
lines: [{ productId: "p-501", quantity: 1 }, { productId: "p-777", quantity: 2 }],
shippingAddress: { street: "Gran Vía 12", postalCode: "28013", city: "Madrid", country: "ES" }
) { id status }
}
- Resolvers in Node.js
The schema says what can be requested; the resolvers say how each field is obtained. A resolver is a function (parent, args, context, info) per field; those not defined resolve by default by reading the property of the same name on the parent object. We will use graphql-yoga, a lightweight GraphQL server that mounts on Express (Apollo Server is the better-known alternative and equivalent for what we do here):
// graphql/server.js (this will live in the mobile BFF from 03-04, not in Orders)
const { createSchema, createYoga } = require('graphql-yoga');
const { readFileSync } = require('node:fs');
const express = require('express');
// REST clients of the internal services (the ones from 03-01)
const ordersApi = require('../clients/ordersClient'); // GET /orders/{id}, POST /orders
const catalogApi = require('../clients/catalogClient'); // GET /products?ids=
const typeDefs = readFileSync(require.resolve('./schema.graphql'), 'utf8');
const resolvers = {
Query: {
// 1. Root resolver: args carries the query arguments; context, what we inject per request
order: (_parent, { id }, context) => ordersApi.getOrder(id, { requestId: context.requestId }),
products: (_parent, { ids }, context) => catalogApi.getProducts(ids, { requestId: context.requestId })
},
Mutation: {
createOrder: (_parent, args, context) =>
ordersApi.createOrder(args, { requestId: context.requestId, idempotencyKey: context.idempotencyKey })
},
OrderLine: {
// 2. Field resolver: 'parent' is the line already fetched by the resolver above.
// It only runs if the query asked for 'product'. THIS is where the N+1 problem is born (section 8).
product: (line, _args, context) => context.productsLoader.load(line.productId)
}
};
const yoga = createYoga({
schema: createSchema({ typeDefs, resolvers }),
// 3. The context is created per request: request id, a fresh DataLoader, and later on the user (07-01)
context: ({ request }) => ({
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
idempotencyKey: request.headers.get('idempotency-key'),
productsLoader: createProductsLoader(request) // section 8
}),
graphqlEndpoint: '/graphql'
});
const app = express();
app.use(yoga.graphqlEndpoint, yoga); // POST /graphql (and GET with the GraphiQL interface for testing)The chain for OrderDetail: the engine calls Query.order (one REST call to Orders), gets the order with its two lines, and for each line calls OrderLine.product. Without further care, that is two calls to Catalog (one per line) on top of the one to Orders; with 20 lines, twenty. It is the N+1 we already avoided in REST with ?ids=; in GraphQL it must be avoided with DataLoader.
- The N+1 problem and DataLoader
DataLoader is a small library (created by Facebook alongside GraphQL) that does two things: it batches every call to .load(id) that happens in the same event-loop tick into a single call to your batch function, and it caches per request so as not to request the same id twice.
// graphql/loaders.js
const DataLoader = require('dataloader');
const catalogApi = require('../clients/catalogClient');
function createProductsLoader(request) {
const requestId = request.headers.get('x-request-id');
// The batch function receives ALL the ids requested in this tick and must return
// an array of the SAME size and in the SAME order (null where one does not exist)
return new DataLoader(async (ids) => {
const products = await catalogApi.getProducts([...ids], { requestId }); // ONE call: GET /products?ids=p-501,p-777
const byId = new Map(products.map(p => [p.id, p]));
return ids.map(id => byId.get(id) ?? null);
}, { maxBatchSize: 100 }); // we honor the batch limit of the Catalog contract (03-01)
}
module.exports = { createProductsLoader };With this, the OrderDetail query makes exactly two REST calls (Orders and Catalog) regardless of the number of lines. Two rules: the DataLoader is created per request (in the context), never globally (the cache would leak data between users and would not refresh), and its batch function must respect the order of the ids.
- When to use GraphQL and its risks
When yes:
- Aggregation for front ends: an app or website with many screens that combine data from several services and evolve at different paces. The client requests what it needs without the back end creating one endpoint per screen. This is the case of the BFF we will see in 03-04: GraphQL is an excellent way to implement it.
- Clients with limited bandwidth (mobile) where over-fetching is costly.
- When the front-end and back-end teams want to decouple their pace: the back end publishes the schema; the front end decides what to query.
When not:
- Between internal services: services do not need flexibility of shape; they need stable, simple contracts (REST or gRPC) or events.
- As the single facade of the whole system ("one graph to rule them all"): it ends up being a schema monolith maintained by a bottleneck team.
Risks that must be managed from day one:
| Risk | Why | Mitigation |
|---|---|---|
| Expensive queries | The client can request customerOrders { lines { product { ... } } } with arbitrary nesting, or huge lists |
Depth and "complexity" limits (each field adds up; queries above a threshold are rejected); maximum limit on lists; timeouts |
| HTTP cache rendered useless | Everything is POST /graphql: CDNs and browsers do not cache by URL |
Client-side cache (Apollo Client, urql), persisted queries (the client sends a hash of a registered query and GET can be used), per-field cache on the server |
| N+1 | One naive resolver per field | DataLoader whenever a field crosses services |
| Partial errors | A query can return data with parts set to null and an errors list; clients not used to it ignore it |
Always handle errors; decide per field whether it is nullable (Product in OrderLine is nullable on purpose: if Catalog fails, the order is still shown) |
| Per-field authorization | There is no longer "one route = one permission" | Check permissions in resolvers or with directives; covered in 07-01 |
- Comparison: REST, gRPC, GraphQL and messaging
| Criterion | REST/JSON | gRPC | GraphQL | Messaging (RabbitMQ) |
|---|---|---|---|---|
| Format | JSON (text) | Protobuf (binary) | JSON (text) | Whatever you want; TechCorp: JSON |
| Contract | OpenAPI (optional, separate from the code) | .proto (mandatory, generates code) |
SDL schema (mandatory, introspectable) | Event schema (AsyncAPI, optional) |
| Transport | HTTP/1.1 or 2 | HTTP/2 | HTTP (usually POST) | AMQP |
| Synchrony | Synchronous | Synchronous (+ streaming) | Synchronous (+ subscriptions) | Asynchronous |
| Temporal coupling | Yes | Yes | Yes | No |
| Response shape | Fixed per endpoint | Fixed per method | Decided by the client | Fixed per event |
| Readability / debugging | Excellent (curl) |
Low (binary; grpcurl needed) |
Good (GraphiQL) | Medium (broker console) |
| HTTP cache | Native (ETag, Cache-Control) |
No | Hard | Not applicable |
| Browsers | Native | Via gRPC-Web + proxy | Native | No (via WebSocket/SSE in a service) |
| Performance | Good | Excellent | Good (depends on resolvers) | Excellent for decoupling and absorbing peaks |
| Use cases | Public APIs, CRUD, simple internal calls | High-volume internal, polyglot, streaming | Aggregation for front ends, BFF | Business events, sagas, integration |
| Tooling | The whole HTTP ecosystem | protoc, grpcurl, Buf |
Apollo, Yoga, GraphiQL, DataLoader | RabbitMQ, amqplib, management console |
| Learning curve | Low | Medium | Medium (high to do it well: N+1, complexity) | Medium (new mental model) |
- TechCorp's decision
Marta and the team leads set the protocol policy:
| Scope | Protocol | Reason |
|---|---|---|
| Public API (web, app, partners) through the gateway on 8080 | REST/JSON with OpenAPI | Universal, cacheable, debuggable; it is what external consumers expect |
| Between services, by default | Events on RabbitMQ | No temporal coupling; it is the saga from 02-05 |
| Between services, when an immediate response is needed | REST/JSON (Orders → Catalog, Orders → Customers) | Moderate volume; reuses the contracts from 03-01 and the same tooling |
| Orders ↔ Inventory | Events today; gRPC candidate if it becomes synchronous | Same team, partnership, potentially high volume; the .proto is already designed |
| Mobile app BFF | GraphQL candidate | Screens that aggregate Orders + Catalog + Payments; mobile bandwidth; decided in 03-04 |
The rule that sums up the decision: no new protocol is introduced until there is a measured problem that REST + events do not solve. gRPC and GraphQL remain known tools with a reserved place, not decisions made out of fashion.
Common Mistakes and Tips
- Adopting gRPC "because it is faster" without measuring. If Orders → Catalog makes 50 calls per second, JSON is not your problem. Measure first (06-04).
- Reusing field numbers in a
.protoor changing the type of an existing field. The old receiver will interpret bytes with the wrong meaning. It is covered in depth in 03-06; for now: new numbers, always. - Forgetting the
deadlinein the gRPC client. Same asfetchwithoutsignal: the call can hang indefinitely. - GraphQL without DataLoader. The N+1 in GraphQL is silent: in development, with two lines, you do not notice; in production, with twenty, it is a storm of requests to Catalog.
- GraphQL without complexity limits. It amounts to exposing an arbitrary query to the Internet. Maximum depth and maximum cost from the first deployment.
- Global DataLoader (shared across requests). It caches one user's data for another and never refreshes. One per request, in the context.
- Using GraphQL between internal services. It adds a resolution layer and loses the simplicity of REST/gRPC without gaining anything: services do not need to choose the shape.
- Ignoring
errorsin the GraphQL response.datacan come with partialnulls and the error be in another list. Clients must look at both.
Exercises
Exercise 1. Add to Inventory's .proto a unary method ReleaseReservation that receives the reservation id and a reason (OUT_OF_STOCK, PAYMENT_REJECTED, PAYMENT_TIMEOUT, CUSTOMER_CANCELLATION) and returns the reservation with status RELEASED. Define the messages with correct field numbers, an enum for the reason, and write the server implementation with the appropriate gRPC codes for: nonexistent reservation, reservation already CONSUMED (cannot be released), and success.
Exercise 2. Extend the GraphQL schema with type Payment { id: ID!, status: PaymentStatus!, amount: Float!, method: String! } and PaymentStatus (the statuses from 02-03), and add the field payment: Payment to Order. Write the Order.payment resolver using a DataLoader over a client paymentsApi.getPaymentsByOrders(orderIds) (GET /payments?orderIds=) and explain why the field must be nullable.
Exercise 3. For each scenario, choose REST, gRPC, GraphQL or messaging and justify it in two sentences: (a) the physical warehouse sends 50,000 stock movements every night from a Java system; (b) an external partner wants to check the status of its orders from its ERP; (c) the internal admin website shows a dashboard with the day's orders, their payments and the associated reservations; (d) when an order is confirmed, the invoice has to be generated in a future Billing service.
Solutions
Solution 1.
enum ReleaseReason {
RELEASE_REASON_UNSPECIFIED = 0;
OUT_OF_STOCK = 1;
PAYMENT_REJECTED = 2;
PAYMENT_TIMEOUT = 3;
CUSTOMER_CANCELLATION = 4;
}
message ReleaseRequest {
string reservation_id = 1;
ReleaseReason reason = 2;
}
service InventoryService {
// ... the previous ones ...
rpc ReleaseReservation (ReleaseRequest) returns (Reservation);
}ReleaseReservation: async (call, callback) => {
const { reservationId, reason } = call.request;
if (!reservationId || reason === 'RELEASE_REASON_UNSPECIFIED') {
return callback({ code: grpc.status.INVALID_ARGUMENT, details: 'reservation_id and reason are required' });
}
const reservation = await reservationsRepository.get(reservationId);
if (!reservation) return callback({ code: grpc.status.NOT_FOUND, details: `Reservation ${reservationId} does not exist` });
if (reservation.status === 'CONSUMED') {
return callback({ code: grpc.status.FAILED_PRECONDITION, details: 'RESERVATION_CONSUMED: cannot be released' });
}
if (reservation.status === 'RELEASED') {
// Idempotent: releasing twice returns the same result without an error
return callback(null, toMessage(reservation));
}
const released = await useCases.releaseReservation(reservationId, reason); // publishes stock.released
callback(null, toMessage(released));
}Important detail: releasing an already-released reservation responds OK with the same reservation, not FAILED_PRECONDITION: the operation is idempotent by design, consistent with the at-least-once redelivery from 03-02.
Solution 2.
enum PaymentStatus { AUTHORIZED CAPTURED REJECTED REFUNDED }
type Payment { id: ID!, status: PaymentStatus!, amount: Float!, method: String! }
extend type Order { payment: Payment }function createPaymentsLoader(request) {
const requestId = request.headers.get('x-request-id');
return new DataLoader(async (orderIds) => {
const payments = await paymentsApi.getPaymentsByOrders([...orderIds], { requestId }); // GET /payments?orderIds=ord-88213,...
const byOrder = new Map(payments.map(p => [p.orderId, p]));
return orderIds.map(id => byOrder.get(id) ?? null);
});
}
// in resolvers:
Order: { payment: (order, _args, ctx) => ctx.paymentsLoader.load(order.id) }Nullable for two reasons: business-wise, a PENDING or STOCK_RESERVED order does not have a payment yet (the saga has not got there); and resilience-wise, if Payments does not respond, GraphQL can return the order with payment: null and an entry in errors instead of failing the whole query. A non-null Payment! would propagate the null upward and null out the entire order.
Solution 3.
(a) gRPC with client streaming (ImportStock(stream Movement)): high volume, a client in another language that generates its code from the same .proto, compact binary for 50,000 messages. Valid alternative: a batch file; but if an API is wanted, gRPC. (b) REST/JSON: external partner, generic ERP, needs curl, OpenAPI and caching; it is the public API through the gateway. (c) GraphQL in an admin BFF: one screen that aggregates three services with a changing shape; with DataLoader over GET /orders, GET /payments?orderIds= and the Inventory contract. Acceptable alternative: a REST composition endpoint in the BFF, if the dashboard is stable. (d) Messaging: Billing subscribes to order.confirmed on a billing.orders queue; Orders neither changes nor knows Billing exists (03-02, exercise 1).
Conclusion
REST/JSON has three clear shortcomings (verbosity, weak contract, fixed response shape) and we have seen the tool that solves each one. gRPC brings a .proto contract from which the code is generated, binary serialization and HTTP/2 with streaming, and fits high-volume internal calls; we have written the InventoryService with ReserveStock and CheckAvailability, its server and client with @grpc/grpc-js and @grpc/proto-loader, and the mapping of its error codes. GraphQL brings a typed schema from which the client requests the exact shape it needs, and fits aggregation for front ends; we have written the orders and products schema, resolvers with graphql-yoga and the solution to the N+1 with a per-request DataLoader. TechCorp's decision: public REST, events between services, gRPC as a candidate for Orders↔Inventory and GraphQL as a candidate for the mobile BFF, adopting nothing until a measured problem justifies it.
That BFF, and the API Gateway on port 8080 we have been talking about since 02-02, are the next topic: which problems a single entry point solves, what it should do (route, authenticate, rate-limit, aggregate) and, above all, what it should not do, how one is implemented in Node.js or declared in Kong or Traefik, and why each type of client (web, mobile) deserves its own backend for frontend.
Microservices Course
Module 1: Introduction to Microservices
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
