Advanced (更新: 2026/6/3)

用 Claude Code 设计微服务:边界、API、Compose 与测试

用 Claude Code 落地微服务:服务边界、API 契约、数据库归属、Docker Compose、可观测性、测试和发布清单。

用 Claude Code 设计微服务:边界、API、Compose 与测试

微服务是一种把大型应用拆成小而独立的服务,并通过 API 或事件协作的设计。Claude Code 适合做这件事,不是因为它能一次生成很多目录,而是因为它能同时检查服务边界、API 契约、数据库归属、本地 Compose 环境、网关、日志、测试和发布步骤。

先说结论:不要为了“看起来现代”而拆微服务。拆分以后,你会立刻面对网络失败、接口兼容、分布式事务、日志追踪、灰度发布和回滚问题。如果业务边界还没有稳定,先用模块化单体更稳。相关基础可以继续看 Claude Code API 开发Docker Compose 实践日志与监控事件驱动架构

把官方资料作为评审锚点:Anthropic Claude Code overviewDocker Compose documentationOpenAPI 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-serviceorders, order_itemsinventory API, order-events读取库存表
inventory-servicestock, reservations暂无读取订单表
notification-servicedelivery logsorder-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-serviceprofiles-serviceaddresses-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 培训与咨询 开始。先选一个业务流程小范围验证,比一次性重写全部系统更安全。

#Claude Code #微服务 #API 设计 #Docker Compose #可观测性 #测试
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。