Claude Code Microservices Guide: Boundaries, APIs, Compose, Testing
Use Claude Code for microservices: boundaries, API contracts, Compose, tests, observability, and rollout.
Microservices are an architecture where a large application is split into small independent services that collaborate through APIs or events. Claude Code is useful here because it can keep service boundaries, API contracts, database ownership, local Docker Compose setup, observability, tests, and rollout notes in view at the same time.
The hard part is not generating folders. The hard part is avoiding a distributed monolith: many small services that still need one shared database, one synchronized release, and one person who understands every hidden dependency. This guide shows a practical workflow you can use before asking Claude Code to write production code.
For adjacent foundations, see API development with Claude Code, Docker Compose with Claude Code, logging and monitoring, and event-driven architecture.
Use the official references as review anchors: Anthropic Claude Code overview, Docker Compose documentation, and OpenAPI Specification 3.1. They make it easier to separate Claude Code suggestions from the actual contract you plan to operate.
Start With Boundaries
Before implementation, ask Claude Code to reason about business capabilities, data ownership, and failure modes.
You are reviewing a migration from a modular e-commerce app to microservices.
Goal:
- Orders change often.
- Inventory must evolve separately because warehouse integration changes monthly.
- Payment and notifications should not block browsing or catalog updates.
Return:
1. Candidate services and responsibilities.
2. Data owned by each service.
3. Which interactions should be synchronous APIs.
4. Which interactions should be events.
5. What should stay in the monolith for now.
Constraints:
- No shared database tables across services.
- No internal table names in public APIs.
- The API gateway must not contain business rules.
- Local development must run with Docker Compose.
This prompt forces Claude Code to explain why a boundary exists. Microsoft Learn’s microservices architecture guide is a useful reference for service autonomy, API contracts, and data isolation. If you plan to run on Kubernetes later, compare your design with the AKS microservices reference architecture.
Define The API Contract First
Do not start with Express handlers. Start with the contract that the gateway, frontend, tests, and downstream consumers can share.
openapi: 3.1.0
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
summary: Create an order after reserving inventory
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customerId, items]
properties:
customerId:
type: string
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
quantity:
type: integer
minimum: 1
responses:
"201":
description: Order accepted
"400":
description: Invalid request
"409":
description: Inventory could not be reserved
Then ask Claude Code to review backward compatibility: which fields can be added safely, which status codes are fixed, and which errors clients must handle. API gateways are useful for routing, authentication, throttling, and traffic policy, but not for hiding unstable service contracts. Microsoft’s API Management gateway overview is a good reference for the gateway role.
Make Data Ownership Explicit
Use a table in the design document and PR description.
| Service | Owns | May call | Must not do |
|---|---|---|---|
| gateway | no business data | order, inventory | calculate discounts or stock |
| order-service | orders, order_items | inventory API, order event stream | read inventory tables |
| inventory-service | stock, reservations | none at first | read orders tables |
| notification-service | delivery logs | order-events | change order state |
If a screen needs order and stock data together, do not join across service databases. Use an API composition endpoint, a read model, a search index, or an event-fed cache depending on latency needs.
For reviews, keep a small service-inventory.json in the repository. Humans own the boundary decision; Claude Code can check whether a change violates the inventory.
{
"services": [
{
"name": "gateway",
"owns": [],
"mayCall": ["order-service", "inventory-service"],
"mustNot": ["store business data", "calculate discounts"]
},
{
"name": "order-service",
"owns": ["orders", "order_items"],
"mayCall": ["inventory-service"],
"mustNot": ["read inventory tables directly"]
},
{
"name": "inventory-service",
"owns": ["stock", "reservations"],
"mayCall": [],
"mustNot": ["change order status"]
}
],
"releaseRules": [
"no shared database tables",
"public APIs hide internal table names",
"every service has healthcheck, logs, tests, and rollback notes"
]
}
Copy-Paste Runnable Local Example
This sample runs a gateway, an order service, an inventory service, and Redis Streams. It is intentionally small but real.
mkdir microservices-demo
cd microservices-demo
mkdir services
npm init -y
npm pkg set type=module
npm install express zod pino redis undici
Create compose.yaml.
services:
gateway:
image: node:22-alpine
working_dir: /workspace
command: node services/service.mjs
environment:
SERVICE: gateway
PORT: 3000
ORDER_URL: http://order-service:3000
INVENTORY_URL: http://inventory-service:3000
ports:
- "8080:3000"
volumes:
- .:/workspace
depends_on:
- order-service
- inventory-service
order-service:
image: node:22-alpine
working_dir: /workspace
command: node services/service.mjs
environment:
SERVICE: order
PORT: 3000
INVENTORY_URL: http://inventory-service:3000
REDIS_URL: redis://redis:6379
volumes:
- .:/workspace
depends_on:
redis:
condition: service_healthy
inventory-service:
condition: service_started
inventory-service:
image: node:22-alpine
working_dir: /workspace
command: node services/service.mjs
environment:
SERVICE: inventory
PORT: 3000
volumes:
- .:/workspace
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
Create services/service.mjs.
import express from "express";
import pino from "pino";
import { createClient } from "redis";
import { request } from "undici";
import { z } from "zod";
import { randomUUID } from "node:crypto";
const service = process.env.SERVICE ?? "inventory";
const port = Number(process.env.PORT ?? 3000);
const log = pino({ name: service });
function createApp(name) {
const app = express();
app.use(express.json());
app.use((req, res, next) => {
req.requestId = req.header("x-request-id") ?? randomUUID();
res.setHeader("x-request-id", req.requestId);
next();
});
app.get("/health", (_req, res) => res.json({ ok: true, service: name }));
return app;
}
function startInventory() {
const app = createApp("inventory");
const stock = new Map([["sku-1", 5], ["sku-2", 2]]);
const ReserveRequest = z.object({
sku: z.string().min(1),
quantity: z.number().int().positive(),
});
app.get("/inventory/:sku", (req, res) => {
res.json({ sku: req.params.sku, quantity: stock.get(req.params.sku) ?? 0 });
});
app.post("/inventory/reservations", (req, res) => {
const parsed = ReserveRequest.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const { sku, quantity } = parsed.data;
const available = stock.get(sku) ?? 0;
if (available < quantity) return res.status(409).json({ error: "insufficient_stock", sku, available });
stock.set(sku, available - quantity);
log.info({ requestId: req.requestId, sku, quantity }, "reserved inventory");
res.status(201).json({ sku, reserved: quantity, remaining: stock.get(sku) });
});
app.listen(port, () => log.info({ port }, "inventory-service started"));
}
async function startOrder() {
const app = createApp("order");
const inventoryUrl = process.env.INVENTORY_URL ?? "http://localhost:3001";
const redis = createClient({ url: process.env.REDIS_URL ?? "redis://localhost:6379" });
redis.on("error", (error) => log.error({ err: error }, "redis error"));
await redis.connect();
const OrderRequest = z.object({
customerId: z.string().min(1),
items: z.array(z.object({
sku: z.string().min(1),
quantity: z.number().int().positive(),
})).min(1),
});
app.post("/orders", async (req, res) => {
const parsed = OrderRequest.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
try {
for (const item of parsed.data.items) {
const response = await request(`${inventoryUrl}/inventory/reservations`, {
method: "POST",
headers: { "content-type": "application/json", "x-request-id": req.requestId },
body: JSON.stringify(item),
});
if (response.statusCode >= 400) {
return res.status(response.statusCode).json(await response.body.json());
}
}
const order = { id: randomUUID(), ...parsed.data, status: "accepted", createdAt: new Date().toISOString() };
await redis.xAdd("order-events", "*", { type: "OrderAccepted", payload: JSON.stringify(order) });
log.info({ requestId: req.requestId, orderId: order.id }, "order accepted");
res.status(201).json(order);
} catch (error) {
log.error({ err: error, requestId: req.requestId }, "order failed");
res.status(500).json({ error: "order_failed" });
}
});
app.listen(port, () => log.info({ port }, "order-service started"));
}
function startGateway() {
const app = createApp("gateway");
const orderUrl = process.env.ORDER_URL ?? "http://localhost:3002";
const inventoryUrl = process.env.INVENTORY_URL ?? "http://localhost:3001";
async function forward(req, res, url) {
const response = await request(url, {
method: req.method,
headers: { "content-type": "application/json", "x-request-id": req.requestId },
body: req.method === "GET" ? undefined : JSON.stringify(req.body),
});
res.status(response.statusCode).send(await response.body.text());
}
app.post("/orders", (req, res) => forward(req, res, `${orderUrl}/orders`));
app.get("/inventory/:sku", (req, res) => forward(req, res, `${inventoryUrl}/inventory/${encodeURIComponent(req.params.sku)}`));
app.listen(port, () => log.info({ port }, "gateway started"));
}
if (service === "inventory") startInventory();
else if (service === "order") await startOrder();
else if (service === "gateway") startGateway();
else throw new Error(`Unknown SERVICE: ${service}`);
Run it.
docker compose up
curl http://localhost:8080/inventory/sku-1
curl -X POST http://localhost:8080/orders \
-H "content-type: application/json" \
-d '{"customerId":"cust-1","items":[{"sku":"sku-1","quantity":2}]}'
docker compose down
Observability, Testing, Rollout
At minimum, keep x-request-id from gateway to downstream services, write structured logs with service name and key IDs, add /health, and define latency/error metrics before the first release. Later you can add OpenTelemetry traces, but do not ship a distributed system with only console strings.
Create a small contract test.
import test from "node:test";
import assert from "node:assert/strict";
import { z } from "zod";
const OrderRequest = z.object({
customerId: z.string().min(1),
items: z.array(z.object({
sku: z.string().min(1),
quantity: z.number().int().positive(),
})).min(1),
});
test("order contract accepts at least one item", () => {
assert.equal(OrderRequest.safeParse({
customerId: "cust-1",
items: [{ sku: "sku-1", quantity: 1 }],
}).success, true);
});
test("order contract rejects empty items", () => {
assert.equal(OrderRequest.safeParse({ customerId: "cust-1", items: [] }).success, false);
});
node --test services/order-contract.test.mjs
Ask Claude Code to review the rollout checklist: API compatibility, database migration ownership, gateway logic, health checks, logs, 400/409/500 behavior, downstream outage behavior, feature flags, rollback, and canary release notes.
Use Cases And Pitfalls
Good use cases include e-commerce order/inventory/payment flows, B2B SaaS domains such as billing and audit logs, and media platforms where ingestion, transformation, search, and delivery scale differently. In each case, the services change at different speeds and failures should be isolated.
Avoid table-based service splitting, shared databases, huge shared domain libraries, business logic in the gateway, and pretending that distributed operations are one ACID transaction. If the domain is still changing daily, keep a modular monolith and let Claude Code help enforce module boundaries first.
The practical decision point is ownership. A microservice is worth introducing when one team can own the API, data migrations, alerts, release notes, and rollback for that boundary without waiting on the whole product. Ask Claude Code to produce a one-page service brief for each candidate: owner, API contract, database tables, events emitted, events consumed, dashboards, SLO, deployment command, rollback command, and the exact failure behavior when a dependency is down.
That brief also protects revenue work. If the service affects checkout, trial signup, ads, reporting, or consultation forms, add the monetization path to the checklist. A technically clean split that breaks attribution or email delivery is still a bad split. For a first pull request, prefer one service inventory file and one contract test over ten generated folders.
If you want reusable CLAUDE.md snippets, review checklists, and API contract templates instead of rebuilding the same notes each time, start from the ClaudeCodeLab practical template products.
If your team wants a hands-on review of boundaries, API contracts, Compose setup, observability, and rollout plans, the Claude Code training and consultation page is the soft next step. Start with one workflow, not a company-wide rewrite.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Receipt Pattern: Record Scope, Proof, and Rollback
A permission receipt pattern for Claude Code: allowed actions, approval boundaries, proof commands, rollback, and revenue CTA checks.
Safe Agent Harness Design for Claude Code and Codex: Permissions, Checks, and Rollback
Build a practical agent harness for Claude Code and Codex with policy, planning, verification, and recovery layers.
Claude Code Subagents: A Practical Guide to Safe Agent Delegation
Claude Code subagent guide for safe parallel article and code work: delegation rules, prompts, pitfalls, and checks.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.