# Microservices development in Cursor

URL: https://developertoolkit.ai/en/cursor-ide/build/microservices/
Description: Build microservices with Cursor Agent: topology rules, Plan Mode boundaries, contract-first APIs gated by lint and breaking-change checks, exemplar scaffolds.

Microservices development in Cursor works when Cursor's Agent sees the service topology and the contracts before it writes code. Project rules describe which service owns what, Plan Mode fixes the boundary, and an OpenAPI or AsyncAPI contract is written and gated first. Each service is accepted on command output: lint, breaking-change diff, contract tests, health check.

This page is for a developer or tech lead who builds services in Cursor every day. The situation is familiar: you ask Agent for a payments service and get a tidy Express app that reads straight from the orders database, invents its own event names, and publishes `paymentComplete` while two consumers listen for `payment.completed`. Nothing fails until staging. The fix is not a better prompt; it is context the Agent always loads and gates it cannot talk its way past.

Sources for this page, checked on 2026-09-26: the Cursor features it uses (Rules, Plan Mode, Worktrees) come from Cursor's documentation as verified on 2026-08-28, and the rule file format follows [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/). On 12 October 2026 Cursor's docs also state that project rules live in `.cursor/rules` as `.mdc` files and that Cursor supports AGENTS.md in the project root and subdirectories, which suits per-service instructions: nested files are combined with those in parent directories, and more specific instructions take precedence. UI labels can differ between Cursor 3.x builds; compare them with your own. The `oasdiff`, `@redocly/cli`, `@asyncapi/cli` and `@pact-foundation/pact` commands were checked against their own repositories and npm on the same date.

## What you get from this microservices workflow in Cursor

- Two kinds of project rule: an always-on topology rule and one file-scoped rule per service, so Agent knows which database and which events belong to which service.
- A Plan Mode prompt that settles a service boundary, its data ownership and its events before any file changes.
- A contract-first loop in which the OpenAPI or AsyncAPI file is written first and blocked in CI when it breaks a consumer.
- A scaffold prompt that copies the conventions of a service you already trust, plus the commands that decide whether the scaffold is done.
- An evidence table that tells a reviewer what to check instead of reading every generated line.
- Recovery steps for the failures specific to multi-service work: shared databases, cycles, event drift and compose networking.

## How should Cursor see several services at once?

Agent can only respect a boundary it can see. How you give it that view depends on how your services are stored.

| Repository layout | What Agent sees by default | What to add |
| --- | --- | --- |
| Monorepo (`orders-service/`, `payments-service/`, …) | Every service | One topology rule plus one file-scoped rule per service, so it does not borrow code across boundaries |
| Polyrepo, a few services you change together | One repository | A multi-root workspace with the services you touch, plus the same rules; see [multi-repo workflows in Cursor](/en/cursor-ide/build/multi-repo-workflows/) for the `.code-workspace` setup |
| Polyrepo, many services owned by other teams | One repository | Copy the other teams' published contracts into `contracts/` and point rules at them instead of opening their code |

Whichever layout you have, write the topology down once. Save it as `.cursor/rules/service-topology.mdc`:

```markdown
---
description: Service topology, data ownership and communication rules for every task
alwaysApply: true
---
# Services
- api-gateway (Node.js, Express): routing, rate limiting, auth. No database.
- orders-service (Node.js, Express): orders. Owns the `orders` Postgres database.
- payments-service (Node.js, Express): payments and refunds. Owns the `payments` database.
- users-service (Python, FastAPI): accounts and profiles. Owns the `users` database.
- inventory-service (Go): stock and reservations. Owns the `inventory` database.

# Hard boundaries
- Database per service. Never query another service's database; call its API or consume its events.
- Contracts live in <service>/docs/openapi.yml and contracts/events/asyncapi.yml. Change the contract first.
- Events are named <entity>.<past-tense-verb> (order.created, payment.completed) and carry event_id and correlation_id.
- Every service exposes GET /health and GET /ready and logs JSON with correlation_id.

# Done means
- `npx @redocly/cli lint <service>/docs/openapi.yml` passes
- the service's own tests pass: `npm test --workspace <service>` (Node services), `pytest` in users-service, `go test ./...` in inventory-service
```

Then add one rule per service that loads only when Agent touches that service's files. Save it as `.cursor/rules/payments-service.mdc`:

```markdown
---
description: Conventions for payments-service
globs: payments-service/**
alwaysApply: false
---
- Stripe webhook signatures are verified with the secret in STRIPE_WEBHOOK_SECRET; never log the payload.
- Consumes: order.created. Publishes: payment.initiated, payment.completed, payment.failed, payment.refunded.
- Every consumer is idempotent on event_id and sends failures to the dead-letter queue.
```

Keep the always-on rule short, because every session pays for it. Save both as `.mdc` files, the format used on [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/), and do not assume a file loads because it is in `.cursor/rules/`: run the canary check from that page first. It shows whether a file was loaded at all.

## Decide the service boundary in Plan Mode first

Plan Mode "creates detailed implementation plans before writing any code", which is what a boundary decision needs: you argue about data ownership while nothing is written yet. Start a new chat, switch it to Plan Mode, and attach the code that currently does the job.

**Copy-paste prompt: plan a service boundary**

```text
@orders-service/src/payments @orders-service/src/db/schema.ts @.cursor/rules/service-topology.mdc

We want to extract payment handling out of orders-service into a new payments-service. Do not write code yet. Produce a plan with:
1. The tables and columns that move to the payments database, and every place in orders-service that reads or writes them today (file and function).
2. For each of those call sites: does it become a synchronous REST call, or does it react to an event? Justify each choice by whether the caller needs the answer before it can respond.
3. The events payments-service publishes and consumes, with payload fields, following our naming rule.
4. A migration order in which each step is deployable on its own and old and new paths can run side by side.
5. The tests that must pass after each step, as exact commands.
List anything you are unsure about as an open question instead of guessing.
```

Read the plan, not a diff. The questions that matter are whether any step leaves two services writing the same table and whether every step has a command that proves it worked. If the boundary is still contested, the architecture discussion belongs in [architecture planning in Cursor](/en/cursor-ide/plan/architecture/) before you build.

## Write the contract first, then gate it

The contract is the part other teams depend on, so it is the part you verify hardest. Generate it before any handler exists, and follow the naming of a contract that already works.

**Copy-paste prompt: draft an OpenAPI contract**

```text
@orders-service/docs/openapi.yml @.cursor/rules/service-topology.mdc

Write payments-service/docs/openapi.yml as OpenAPI 3.1, following the schema naming, error envelope and header conventions in the orders-service contract exactly.
Endpoints:
- POST /v1/payments: create a payment for an order (order_id, customer_id, amount in minor units, currency). Returns 201, or 409 if a payment for that order_id already exists.
- GET /v1/payments/{id}: payment status.
- POST /v1/payments/{id}/refund: full or partial refund (amount, reason). 422 if the amount exceeds what was captured.
- POST /webhooks/stripe: Stripe webhook receiver.
Every operation takes and returns the x-correlation-id header and documents 400, 404 and 500 with the shared error schema. Include one realistic example per request and response.
Then run `npx @redocly/cli lint payments-service/docs/openapi.yml` and fix every error before you stop. Do not write any handler code.
```

Events deserve the same treatment. If you keep an AsyncAPI file for RabbitMQ exchanges or Kafka topics, ask Agent to add the new messages there and validate it with `npx @asyncapi/cli validate contracts/events/asyncapi.yml` (@asyncapi/cli 6.x requires Node 24 or later).

Two commands then turn the contract into a gate that runs on every pull request, whether a person or Agent changed it. Run them in the terminal or in CI:

```bash
# 1. The contract is well formed and follows your lint rules
npx @redocly/cli lint payments-service/docs/openapi.yml

# 2. The change does not break an existing consumer (compare with the main branch)
#    (skipped for a new service, which has no base on main yet)
if git cat-file -e origin/main:payments-service/docs/openapi.yml 2>/dev/null; then
  git show origin/main:payments-service/docs/openapi.yml > /tmp/openapi-base.yml &&
  oasdiff breaking --fail-on ERR /tmp/openapi-base.yml payments-service/docs/openapi.yml
fi
```

`oasdiff breaking --fail-on ERR` exits with code 1 when it finds a definite breaking change, such as a removed field or a newly required parameter. Install it with `brew install oasdiff`, `go install github.com/oasdiff/oasdiff@latest`, or the `tufin/oasdiff` Docker image. For a brand-new service there is no base on `main` yet, so the `if` skips the diff and only the lint runs until the first merge.

Once the contract passes, implementation becomes a narrow task: "implement the handlers in `payments-service` so they match `@payments-service/docs/openapi.yml` exactly, using the validation middleware from `@orders-service/src/routes/orders.ts`". The contract, not the prompt, now defines the API surface.

## Scaffold a new service from one you already trust

A generic template misses your conventions. Pointing Agent at a service your team already runs in production gives it concrete middleware, logging and Docker patterns to copy.

**Copy-paste prompt: scaffold a service from an exemplar**

```text
@orders-service/src/app.ts @orders-service/src/middleware @orders-service/Dockerfile @docker-compose.yml @payments-service/docs/openapi.yml

Create payments-service with the same project layout, middleware order, correlation-id handling, JSON logging, error handler, health and readiness endpoints, and multi-stage Dockerfile as orders-service. Differences:
- Routes come from payments-service/docs/openapi.yml; stub each handler to return 501 for now.
- Configuration is read from environment variables only (DATABASE_URL, RABBITMQ_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET). No default values for secrets.
- Add a payments-service entry to docker-compose.yml on the existing network, with a healthcheck that calls GET /health.
- Add one test per endpoint that asserts the 501 stub and the x-correlation-id echo.
When you finish, run the commands below and paste their output. Stop and tell me if any fails.
npm test --workspace payments-service
docker compose up -d --wait payments-service
curl -fsS http://localhost:3004/health
```

The port `3004` is an example; use the one your compose file assigns. The scaffold is done when those three commands pass, the Redocly lint passes, and `git diff --stat` shows no file outside `payments-service/` except the compose entry. That last check is the cheap way to catch Agent "helpfully" editing orders-service.

## Choose synchronous calls or events on purpose

Agent can infer a communication style from the wording of your prompt: "Notify the user when an order is placed" reads like a function call and may come back as a direct HTTP call to the notification service. Decide the style yourself and write it into the prompt.

| The caller… | Use | Name it in the prompt as |
| --- | --- | --- |
| Needs the answer before it can respond (stock check during checkout) | Synchronous REST or gRPC with a timeout and a circuit breaker | "Call inventory-service `GET /v1/stock` with a 2-second timeout" |
| Only needs to know the work will happen (send a receipt) | An event | "Publish `order.created`; notification-service consumes it" |
| Starts a multi-step business transaction (order, then payment, then stock) | A saga of events with compensation | Build one step at a time; see [distributed systems development with AI](/en/workflows/build/microservices/) |

For an event consumer, ask for the failure behaviour and a test that proves it, not only the happy path.

**Copy-paste prompt: an idempotent event consumer with proof**

```text
@notification-service/src/events/consumer.ts @contracts/events/asyncapi.yml @.cursor/rules/payments-service.mdc

In payments-service, add a RabbitMQ consumer for order.created that follows the notification-service consumer pattern exactly (ack/nack handling, dead-letter queue, structured logging with the event's correlation_id).
On each message: create a Stripe PaymentIntent with the order_id as the idempotency key, store it, and publish payment.initiated as defined in the AsyncAPI file.
Write these tests first and show them to me failing before you implement:
1. The same event delivered twice creates one PaymentIntent and publishes one payment.initiated.
2. A Stripe error sends the message to the dead-letter queue and publishes nothing.
3. A payload that does not match the AsyncAPI schema is rejected without calling Stripe.
Then implement until `npm test --workspace payments-service` passes.
```

## How do you verify a multi-service change without reading every line?

A reviewer of a multi-service pull request needs evidence at the boundaries, because that is where the bugs appear. The table below is the evidence bundle to require; each row is a command whose output the pull request links. The idea behind it is covered in [reading evidence instead of code](/en/ladder/evidence-not-diffs/).

| Evidence | Command or source | What it proves | Who signs off |
| --- | --- | --- | --- |
| Contract is valid | `npx @redocly/cli lint …` and `npx @asyncapi/cli validate …` | The contract is well formed | CI |
| No consumer breaks | `oasdiff breaking --fail-on ERR` against `origin/main` | No removed fields or newly required inputs | CI; the owning team approves any intended break |
| Consumers still agree | The gateway's Pact tests (`npm test` in `api-gateway`) write a pact file; payments-service's build replays it with Pact's `Verifier.verifyProvider()` and fails on a mismatch | The gateway's expectations match what payments-service returns | CI on both sides |
| Service behaves | The service's own test suite, including the idempotency tests above | Handlers and consumers match the contract, including failure paths | CI |
| It runs with its neighbours | `docker compose up -d --wait` plus one end-to-end request through the gateway | Networking, configuration and health checks work together | The author, with output pasted in the PR |
| It stayed in its lane | `git diff --stat origin/main` | No edits in services the task did not name | The reviewer, in 10 seconds |

Pact is one dev dependency on both sides: `npm install -D @pact-foundation/pact` (17.1.4 on npm, checked on 2026-09-26).

Human review then concentrates on the contract diff and the migration order, which are short and consequential. For automated pull request review on top of these gates, see [Bugbot](/en/cursor-ide/ship/bugbot/); for wiring the commands into a pipeline, see [CI/CD with Cursor](/en/cursor-ide/ship/ci-cd/).

## Work on several services in parallel

Cursor's Worktrees "let Agent work in isolated Git checkouts", so one agent can build the payments-service handlers while another updates the gateway client, each on its own branch. Give each agent one service and the shared contract as read-only input. Merge the contract change first, then the provider, then the consumers, and rerun the evidence table after each merge. [Parallel agents in the Agents Window](/en/cursor-ide/build/agents-window/) covers how to start and supervise them.

## Debug a request that crosses services

When a request fails somewhere between the gateway and a database, collect the logs for one correlation ID from each service and let Agent line them up. Use a Plan Mode chat so it investigates and proposes before it edits anything.

**Copy-paste prompt: correlate logs across services**

```text
These are the logs for correlation_id abc-123-def from three services, in UTC.

api-gateway:
10:15:32.101 INFO  POST /checkout received
10:15:32.140 INFO  inventory-service responded 200 in 38ms
10:15:33.002 INFO  calling orders-service POST /v1/orders
10:15:38.004 ERROR orders-service timeout after 5000ms

orders-service:
10:15:33.010 INFO  POST /v1/orders received
10:15:33.015 INFO  acquiring row lock for customer cust_456
10:15:38.020 ERROR lock acquisition timeout after 5000ms

payments-service:
(no entries for this correlation_id)

Build a timeline across the services. Identify the first point where behaviour diverges from a normal checkout, the likely root cause, and what evidence would confirm it (a query, a metric, or a log line to add). Propose a fix and a test that would have failed before it. Do not edit files.
```

A missing service in the timeline, like payments-service here, is itself a finding: either the call never happened or the correlation ID was dropped. For tracing and dashboards that make this routine, see [monitoring with Cursor](/en/cursor-ide/operate/monitoring/).

## What breaks when Agent builds microservices?

**Agent queries another service's database.** The topology rule did not load, or the prompt attached the other service's schema file. Recovery: run the canary check from the project rules page, remove the foreign schema from the chat, and restate the boundary: "payments-service reads user data only through `GET /v1/users/{id}`." Add a lint rule that fails on imports between service directories so the next attempt is caught mechanically. With ESLint flat config, scope the core `no-restricted-imports` rule to one service (ESLint 10.11.0, checked on 2026-09-26):

```js
// eslint.config.js
export default [
  {
    files: ['orders-service/**'],
    rules: {
      'no-restricted-imports': ['error', {
        patterns: [{ group: ['**/payments-service/**'], message: 'Call the payments API or consume its events.' }],
      }],
    },
  },
];
```

**Two services now call each other.** payments-service calls orders-service, which calls payments-service. Recovery: in a Plan Mode chat, ask Agent to list every outbound call per service from the code and report cycles. Break each cycle with an event, or move the shared step into the service that owns the data.

**Event payloads drift between publisher and consumer.** A consumer breaks without an error because a field was renamed. Recovery: make the AsyncAPI file the single definition, attach it in every event prompt, and add the schema-rejection test from the consumer prompt to each consumer.

**The new container cannot reach its neighbours.** Agent wrote a standalone Dockerfile and a compose entry on the default network. Recovery: attach the existing `docker-compose.yml`, name the network, and require `docker compose up -d --wait` output before you accept the scaffold.

**Agent keeps "fixing" the contract to match its code.** A failing Redocly or oasdiff check tempts it to edit the spec. Recovery: say in the prompt that the contract is fixed and only handlers may change, and let CI reject any contract diff that lacks an approval from the owning team.

**The context fills up before the real task starts.** Four services, their schemas and their compose file are attached to every chat. Recovery: attach only the services the task names, keep other teams' contracts in `contracts/`, and let the file-scoped rules load the rest; [context management in Cursor](/en/cursor-ide/quick-start/context-management/) shows how to see what a session carries.

## How Claude Code and Codex handle the same work

The contract-first loop, the evidence table and the failure modes are the same in every tool; only the context mechanism differs (`CLAUDE.md` in Claude Code, `AGENTS.md` in Codex). The cross-tool version of this workflow, with sagas, trace propagation, schema migrations and the relevant MCP servers, is [distributed systems development with AI](/en/workflows/build/microservices/).

## Where to go next with microservices in Cursor

- [Multi-repo workflows](/en/cursor-ide/build/multi-repo-workflows/) — Set up a multi-root workspace and contract rules for polyrepo services.
- [API integration](/en/cursor-ide/build/api-integration/) — Build and test the clients that call your services.
- [CI/CD with Cursor](/en/cursor-ide/ship/ci-cd/) — Run the contract and test gates in per-service pipelines.
- [Monitoring](/en/cursor-ide/operate/monitoring/) — Trace requests across services once they are in production.
