用 Claude Code 设计微服务:边界、API、Compose 与测试
用 Claude Code 落地微服务:服务边界、API 契约、数据库归属、Docker Compose、可观测性、测试和发布清单。
微服务是一种把大型应用拆成小而独立的服务,并通过 API 或事件协作的设计。Claude Code 适合做这件事,不是因为它能一次生成很多目录,而是因为它能同时检查服务边界、API 契约、数据库归属、本地 Compose 环境、网关、日志、测试和发布步骤。
先说结论:不要为了“看起来现代”而拆微服务。拆分以后,你会立刻面对网络失败、接口兼容、分布式事务、日志追踪、灰度发布和回滚问题。如果业务边界还没有稳定,先用模块化单体更稳。相关基础可以继续看 Claude Code API 开发、Docker Compose 实践、日志与监控 和 事件驱动架构。
把官方资料作为评审锚点:Anthropic Claude Code overview、Docker Compose documentation 和 OpenAPI Specification 3.1。这样可以把 Claude Code 的建议和团队真正要运行的契约分开看。
先让 Claude Code 判断边界
不要直接说“帮我把电商系统拆成微服务”。更好的提示词是要求它说明为什么拆、数据归谁、失败时怎么处理。
你是电商系统微服务拆分的架构审查员。
业务背景:
- 订单流程变化频繁。
- 库存需要和仓库系统独立迭代。
- 支付和通知失败时,不应该阻塞商品浏览。
请输出:
1. 服务候选和职责。
2. 每个服务拥有的数据。
3. 同步 API 和异步事件的边界。
4. 暂时不应该拆出去的功能。
5. 第一轮迭代的最小可运行方案。
约束:
- 禁止共享数据库表。
- API 不能暴露内部表名。
- Gateway 不能写业务规则。
- 本地必须能用 Docker Compose 启动。
微软的 Microservices architecture guide 对服务自治、API 契约和数据隔离有清晰解释;如果以后要上 Kubernetes,也可以对照 AKS microservices reference architecture。网关的职责可以参考 API Management gateway overview。
API 契约和数据归属
先写契约,再写代码。下面这个 OpenAPI 片段就是订单服务的最小入口。
openapi: 3.1.0
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
summary: Create an order after reserving inventory
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
"409":
description: Inventory could not be reserved
同时把数据归属写成表格,放进设计文档和 PR 描述。
| 服务 | 拥有的数据 | 可以调用 | 禁止事项 |
|---|---|---|---|
| gateway | 无业务数据 | order, inventory | 计算库存或折扣 |
| order-service | orders, order_items | inventory API, order-events | 读取库存表 |
| inventory-service | stock, reservations | 暂无 | 读取订单表 |
| notification-service | delivery logs | order-events | 直接修改订单状态 |
如果页面需要同时展示订单和库存,不要跨库 JOIN。可以用 API 聚合、读模型、搜索索引或事件驱动缓存。
为了让评审稳定,可以在仓库里放一个小的 service-inventory.json。边界由人决定,Claude Code 负责检查某个变更是否违反了这个台账。
{
"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"
]
}
可复制运行的最小示例
mkdir microservices-demo
cd microservices-demo
mkdir services
npm init -y
npm pkg set type=module
npm install express zod pino redis undici
创建 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
创建 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 baseApp(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 inventory() {
const app = baseApp("inventory");
const stock = new Map([["sku-1", 5], ["sku-2", 2]]);
const Reserve = 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 = Reserve.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const available = stock.get(parsed.data.sku) ?? 0;
if (available < parsed.data.quantity) return res.status(409).json({ error: "insufficient_stock", available });
stock.set(parsed.data.sku, available - parsed.data.quantity);
log.info({ requestId: req.requestId, sku: parsed.data.sku }, "reserved");
res.status(201).json({ sku: parsed.data.sku, remaining: stock.get(parsed.data.sku) });
});
app.listen(port, () => log.info({ port }, "inventory started"));
}
async function order() {
const app = baseApp("order");
const redis = createClient({ url: process.env.REDIS_URL ?? "redis://localhost:6379" });
await redis.connect();
const Order = 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 = Order.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
for (const item of parsed.data.items) {
const response = await request(`${process.env.INVENTORY_URL}/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" };
await redis.xAdd("order-events", "*", { type: "OrderAccepted", payload: JSON.stringify(order) });
res.status(201).json(order);
});
app.listen(port, () => log.info({ port }, "order started"));
}
function gateway() {
const app = baseApp("gateway");
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, `${process.env.ORDER_URL}/orders`));
app.get("/inventory/:sku", (req, res) => forward(req, res, `${process.env.INVENTORY_URL}/inventory/${encodeURIComponent(req.params.sku)}`));
app.listen(port, () => log.info({ port }, "gateway started"));
}
if (service === "inventory") inventory();
else if (service === "order") await order();
else if (service === "gateway") gateway();
运行:
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
上线前的判断基准
上线前不要只看服务数量,而要看团队是否能独立修改、测试和回滚每个服务。Claude Code 可以帮你把一个业务流程写成检查表:哪个服务拥有数据,哪个 API 是公开契约,哪个事件可以重试,哪个失败必须人工介入。这个表比“要不要拆成五个服务”的讨论更有价值。
一个实用做法是先选订单、库存、通知这种边界清楚的流程,只拆出最小闭环。每个服务都要有独立健康检查、日志字段、契约测试和回滚步骤。等这些运维动作跑顺之后,再考虑拆分更复杂的计费、权限或搜索服务。这样可以避免一开始就把系统拆碎,却没有任何团队能负责端到端质量。
如果现有系统还是单体应用,可以先用“模块边界”练习,而不是马上拆部署。让 Claude Code 读取目录结构、路由、数据库表和主要用例,输出候选服务边界,再要求它说明每个边界的输入、输出、数据拥有者和失败处理。只有当某个模块的变更频率、扩缩容需求、团队责任和数据模型都相对独立时,才值得进入真正的服务拆分。
迁移时建议保留一个反悔路径。第一阶段只把契约写出来,并在单体内用清晰模块调用;第二阶段用 Docker Compose 本地跑出一个旁路服务;第三阶段再把少量真实流量通过 Feature Flag 切过去。Claude Code 可以为每个阶段生成验证脚本和回滚清单,但不要让它跳过人工评审。微服务的收益来自可控边界,不来自一次性大爆炸式重写。
三个更具体的落地场景
第一个场景是电商或预约系统。订单、库存、支付、通知的失败影响不同:库存同步慢不应该让商品浏览停止,通知服务故障也不应该阻止订单写入。让 Claude Code 先画出这些边界,再生成 API 和测试,比直接生成四个服务目录更安全。
第二个场景是 B2B SaaS。计费、权限、审计日志、工作流通常由不同团队维护,也有不同的合规要求。这里最容易出错的是把 tenant、role、billing plan 到处复制。更好的做法是先定义“谁拥有这些事实”,再决定其他服务通过 API、事件还是只读投影读取。
第三个场景是内容平台。入稿、图片转换、搜索索引、推荐和分析的负载形态完全不同。把重处理任务从编辑体验中拆出去很有价值,但也要准备重试、幂等键和失败队列。否则一次转换失败就会变成编辑、发布、搜索三方互相甩锅。
失败例和迁移顺序
最常见的失败是按数据库表拆服务,例如 users-service、profiles-service、addresses-service。这看起来整齐,但一个页面可能要串行调用多个服务,延迟和故障面都会扩大。边界应该来自业务能力,而不是 ER 图。
第二个失败是共享数据库。它会让每个服务看起来独立,实际却必须同时理解同一套 schema。Claude Code 生成迁移脚本时,要明确让它检查“这个 migration 属于哪个服务”,并禁止其他服务直接读取内部表。
第三个失败是把领域模型放进一个巨大的 shared library。日志格式、错误包装、认证 helper 可以共用,但订单状态、库存规则、计费规则不应该变成全服务共同发布的包。否则每次修改都会变成隐性的全系统发布。
实际迁移建议是从模块化单体开始,先写 service-inventory.json、OpenAPI 契约和契约测试;再用 Docker Compose 跑一个旁路服务;最后通过 Feature Flag 只切少量流量。每一步都要保留回滚脚本和人工验收点。
观测、测试和发布
至少要保留 x-request-id、结构化日志、服务名、关键业务 ID、/health 和错误率指标。契约测试可以从请求 schema 开始,先验证空商品列表会被拒绝、合法订单会被接受。发布前让 Claude Code 检查 API 兼容性、数据库迁移归属、网关是否混入业务逻辑、400/409/500 分支、Redis 停机时的行为、Feature Flag、灰度和回滚。
适合微服务的场景包括:电商订单、库存、支付和通知;B2B SaaS 的计费、权限、审计;内容平台的入稿、转换、搜索和分发。常见坑包括按表拆服务、共享数据库、共享领域模型库过大、把业务规则塞进 Gateway、没有可观测性就上线。
如果要用英文关键词描述,这里的 use case 是“需要独立变更、独立扩缩容、独立失败处理的业务能力”,而 pitfall 是“服务目录变多,但责任、数据和回滚没有变清楚”。让 Claude Code 为每个候选服务写出 owner、API、数据表、事件、告警、rollback 命令和 dependency failure 行为,可以把讨论从抽象架构拉回可验证事实。对于会影响付款、试用注册、广告计量或咨询表单的服务,还要把收入路径写进清单,否则技术拆分可能悄悄伤害转化。
如果你需要可复用的 CLAUDE.md 片段、评审清单和 API 契约模板,可以先从 ClaudeCodeLab 的 实务模板产品 开始。
如果你想把真实项目的边界、API 契约、Compose 和监控方案一起审一遍,可以从 Claude Code 培训与咨询 开始。先选一个业务流程小范围验证,比一次性重写全部系统更安全。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。