SDD for Go Microservices: API Contracts Between Services
How to apply Specification-Driven Development across Go microservices. Design inter-service API contracts, event schemas, and consumer-driven contract testing with Claude Code so teams can deploy independently.
SDD microservice golang API contract work is where specification-driven development earns its keep. In a monolith, one spec governs one endpoint. In microservices, each feature can touch three, four, or more services — each with contracts that need to be agreed upon.
This is where SDD becomes crucial. Well-defined inter-service API contracts are the difference between teams that can deploy independently and teams that always need to “coordinate before deploying.” In this article we map how SDD works in a microservice environment, with concrete examples from Santekno Shop.
16.1 Three Types of Contracts in Microservice Environments
Before writing any spec, it helps to know that inter-service communication is not one shape but three — and each needs its own contract. The list below names the three types you will specify in a microservice system.
- Contract 1: Synchronous HTTP API Contract — between services communicating request-response.
- Contract 2: Async Event Contract — for Kafka/messaging communication, including schema, versioning, and consumer guarantees.
- Contract 3: Data/Query API Contract — instead of shared databases, explicit query APIs.
The takeaway: every arrow between two services in your architecture diagram maps to exactly one of these three contract types, and each type has its own spec format. Naming the type first tells you which template to reach for.
16.2 Consumer-Driven Contract Testing
The most robust approach in microservice SDD is Consumer-Driven Contract Testing (CDCT): the consumer defines what it needs, and the provider proves it satisfies that need. The Go test below is how a Notification Service declares its expectations of the Order Service using Pact.
1// Notification Service (Consumer) defines its contract
2func TestOrderServiceContract(t *testing.T) {
3 mockProvider := dsl.NewPact(dsl.Config{
4 Consumer: "notification-service",
5 Provider: "order-service",
6 })
7
8 mockProvider.
9 AddInteraction().
10 Given("an order has been created").
11 UponReceiving("ORDER_CREATED event").
12 WithRequest(dsl.Request{
13 Method: "GET",
14 Path: dsl.String("/internal/v1/orders/123"),
15 }).
16 WillRespondWith(dsl.Response{
17 Status: 200,
18 Body: map[string]interface{}{
19 "order_id": dsl.Like("123"),
20 "total_amount_cents": dsl.Like(150000),
21 },
22 })
23
24 err := mockProvider.Verify(t, func() error {
25 client := order.NewClient("http://localhost:" + mockProvider.Server.Port)
26 order, err := client.GetOrder(context.Background(), "123")
27 assert.NoError(t, err)
28 return err
29 })
30 assert.NoError(t, err)
31}Notice that the consumer — not the provider — writes this test, and it only asserts the fields it actually uses. That inversion is the whole point: the provider then runs the same contract to guarantee it never breaks a field a real consumer depends on.
16.3 Multi-Service Feature Spec
When a feature spans multiple services, the spec must describe the whole flow, not just one service’s slice. The spec excerpt below captures the interaction flow and the cross-service acceptance criteria for cancelling an order.
1## Feature: Customer Cancels Order (Multi-Service)
2
3### Service Interaction Flow
4Customer → DELETE /orders/{id} → Order Service
5Order Service → UPDATE status + stock restore → PUBLISH ORDER_CANCELLED
6Kafka → Notification Service → send email
7Kafka → Analytics Service → update metrics
8
9### Cross-Service ACs
10- XAC1: Cancellation email sent within 60 seconds
11- XAC2: Analytics dashboard updated within 5 minutes
12- XAC3: If Notification Service down, Order Service still returns 204
13- XAC4: If Analytics Service down, Order Service still returns 204The key insight from this spec is XAC3 and XAC4: they encode that downstream services are best-effort. Writing that down prevents a later “why did cancel fail just because email was down?” incident — the spec makes the resilience boundary explicit.
16.4 Event Schema Versioning Policy
Event schemas change over time, and the danger is that a producer change silently breaks a consumer. The policy below draws a hard line between changes that are safe and changes that require a migration window.
1### Backward-Compatible Changes (no version bump needed)
2- Add optional new fields
3- Add new enum values (consumers must handle unknown)
4- Relaxing validation
5
6### Breaking Changes (require new version + migration period)
7- Remove or rename fields
8- Change data types
9- Tighten validation
10
11### Migration Process
121. Publish v1 and v2 simultaneously for 30 days
132. After 30 days: deprecate v1 (X-Schema-Deprecated: true header)
143. After 60 days: remove v1 supportThe practical rule to carry away: adding is safe, removing or changing is breaking. When a change falls in the breaking column, the 30/60-day dual-publish window buys every consumer time to migrate before the old format disappears.
16.5 Contract Compatibility Check
Once you have a producer schema and several consumers, you want a mechanical way to confirm they still agree. The prompt below asks Claude Code to build a compatibility matrix across one producer and two consumers.
1I have three services with event contracts:
2
3Order Service publishes ORDER_CANCELLED: [schema]
4Notification Service consumes it: [consumer code]
5Analytics Service consumes it: [consumer code]
6
7Do a contract compatibility check:
81. Are all fields consumed by Notification Service in the event schema?
92. Are all fields consumed by Analytics Service in the event schema?
103. Any type mismatches between producer and consumers?
11
12Output: compatibility matrix
13| Field | Schema | Notif Consumer | Analytics Consumer | Compatible? |The output — a field-by-field matrix — turns “does everyone still agree?” from a nervous guess into a checklist. Run it before every schema change and you catch the missing-field mismatch at review time instead of in production.
16.6 Saga Pattern Spec
Features that need a distributed transaction can’t rely on a database transaction across services; a Saga coordinates them instead. The spec below lays out the forward steps and, critically, the compensating steps that undo work when a later step fails.
1## Saga: Create Order
2
3Step 1: Order Service → Create order (DRAFT status)
4Step 2: Inventory Service → Reserve stock
5 Success: STOCK_RESERVED event | Failure: STOCK_RESERVATION_FAILED
6Step 3 (if reserved): Payment Service → Charge payment
7 Success: PAYMENT_CHARGED | Failure: PAYMENT_FAILED
8Step 4 (if charged): Order Service → Confirm order
9
10### Compensating Transactions
11If payment fails: Inventory Service releases reserved stock
12If confirmation fails: Payment Service refunds, Inventory releases
13
14### ACs
15- AC1: Happy path completes in < 5 seconds
16- AC2: If payment fails, stock returned within 30 seconds
17- AC3: No double charge on retry (idempotency)The compensating-transaction section is what separates a real Saga spec from a wish list: for every forward action you specify how it is undone. AC3’s idempotency requirement is the other non-negotiable — retries are inevitable, and the spec forces you to design for them.
16.7 CI/CD Contract Testing
A contract is only trustworthy if CI verifies it on every change. The workflow snippet below runs provider verification so the Order Service is proven against all published consumer contracts before it can merge.
1jobs:
2 provider-contract-test:
3 steps:
4 - name: Verify Order Service satisfies consumer contracts
5 run: |
6 pact-provider-verifier \
7 --provider=order-service \
8 --provider-base-url=http://localhost:8080 \
9 --pact-broker-url=http://pact-broker \
10 --publish-verification-resultsWiring this into CI means a breaking change to the provider fails the pipeline, not a downstream consumer at runtime. The --publish-verification-results flag also feeds the broker, so every team can see at a glance which provider version satisfies which contract.
16.8 Contract-First for New Internal APIs
When two teams need a brand-new endpoint between their services, starting from the contract lets both work in parallel. The prompt below asks Claude Code to draft the spec, contract tests, and compatibility considerations before a line of implementation exists.
1Team A (Order Service) needs bulk stock check from Product Service.
2No such endpoint exists yet.
3
4Help create:
51. OpenAPI spec for new endpoint:
6 POST /internal/v1/products/check-bulk-stock
7 Input: [{product_id, quantity}]
8 Output: [{product_id, available, current_stock}]
9
102. Is request-response or event-driven more appropriate here?
11
123. Contract tests that must pass before Order Service can use it
13
144. Backward compatibility considerations
15
16Then Team B (Product Service) implements based on this contract,
17while Team A develops using mock from the contract.The unlock here is parallelism: once the contract exists, Team A codes against a mock and Team B implements the real endpoint, and they meet at a contract both already agreed on. Contract-first turns a sequential dependency into two independent tracks.
16.9 Spec Drift Detection for Events
Even with contracts in place, a consumer can start reading a field the schema never promised. The shell script below extracts the fields a consumer actually uses and checks each one against the event schema.
1#!/bin/bash
2# scripts/check-event-contract.sh
3
4NOTIF_FIELDS=$(grep -r "event\." notification-service/internal/ | \
5 grep -o 'event\.[A-Za-z_]*' | sort -u)
6
7SCHEMA_FILE="specs/contracts/order-events.md"
8
9for field in $NOTIF_FIELDS; do
10 field_name="${field#event.}"
11 if ! grep -q "$field_name" "$SCHEMA_FILE"; then
12 echo "Field '$field_name' used by notification-service not in event schema"
13 exit 1
14 fi
15done
16
17echo "All consumer fields found in event schema"This is a cheap first line of defense: it fails the build the moment a consumer depends on a field that isn’t in the contract. It won’t catch type mismatches — that’s what the compatibility matrix in 16.5 is for — but it stops the most common event drift dead.
16.10 Service Dependency Mapping
In a complex microservice estate, you can’t reason about failure modes without seeing the dependency graph. The prompt below asks Claude Code to build that map and then analyze it for weak points.
1Create a service dependency map for Santekno Shop:
2
3Order Service:
4- Synchronous calls TO: Product Service (stock check), User Service (address validation)
5- Publishes TO Kafka: ORDER_CREATED, ORDER_CANCELLED, ORDER_SHIPPED
6- Consumes FROM Kafka: PAYMENT_CHARGED, PAYMENT_FAILED
7
8From this map:
91. Identify single points of failure
102. Identify circular dependencies (if any)
113. Recommend blast radius isolation if Order Service goes downThe value of the map isn’t the diagram — it’s the three questions it answers: where a single failure cascades, where a circular dependency hides, and how to contain the blast radius. Generate it once per release and you catch architectural risk before it becomes an outage.
16.11 Testing Multi-Service Spec
Cross-service acceptance criteria (XAC1–XAC4) only count if a test actually exercises the whole path. The Go test below cancels an order and then verifies both the event publication and the downstream email.
1func TestCancelOrder_MultiService_NotificationSent(t *testing.T) {
2 orderID := createPendingOrder(t)
3
4 // Cancel the order
5 resp := cancelOrder(t, orderID)
6 assert.Equal(t, http.StatusNoContent, resp.StatusCode)
7
8 // Verify ORDER_CANCELLED event published to Kafka
9 event := consumeFromKafka(t, "order-events", 5*time.Second)
10 assert.Equal(t, "ORDER_CANCELLED", event.EventType)
11
12 // XAC1: Email sent within 60 seconds
13 emailSent := waitForEmail(t, testEmail, 60*time.Second)
14 assert.True(t, emailSent)
15}Note how each assertion maps directly back to a spec line: the 204 to the primary behavior, the Kafka consume to the event contract, and waitForEmail to XAC1’s 60-second budget. A multi-service test that traces to named ACs is what makes the spec verifiable rather than aspirational.
16.12 Kafka Topic Configuration Spec
Infrastructure is part of the contract too — partition count and ordering guarantees change how consumers must be written. The spec below pins the topic configuration so those guarantees are explicit rather than accidental.
1## Topic: order-events
2- Partitions: 12 (parallelism)
3- Replication factor: 3 (resilience)
4- Retention: 7 days (consumer lag recovery)
5- Partition key: order_id (ordering per order)
6- Max message size: 1MB
7
8## Consumer Groups
9- notification-service: independent offset group
10- analytics-service: independent offset group
11
12## Error Handling
13- Max retries: 3
14- Dead letter topic: order-events-dlq (30 days retention)The load-bearing line is the partition key: order_id guarantees ordering per order but not globally. Writing that into the spec tells every consumer author exactly what ordering they can and cannot assume — the single most misunderstood property of Kafka.
16.13 Canary Deployment with Contract Verification
Contract verification shouldn’t stop at CI; it should gate the rollout itself. The pipeline below refuses to send production traffic to a new Order Service version until it has passed contract verification.
1stages:
2 - name: contract-check
3 run: pact-provider-verifier --provider=order-service --provider-base-url=http://canary
4
5 - name: canary-deploy
6 only_if: contract-check == passed
7 run: |
8 kubectl set image deployment/order-service order-service:new-version
9 # Route 5% traffic to new versionThe only_if: contract-check == passed guard is the whole safety mechanism: even a canary at 5% traffic never sees the new version unless contracts hold. Deployment gating on contracts turns “we tested it in CI” into “production literally cannot receive a contract-breaking build.”
16.14 Tips & Gotchas
Before the summary, here are the field-tested reminders that keep microservice SDD from going sideways. Read them as a pre-flight checklist for your next inter-service change.
- Tip 1: Contract-first before code for inter-service APIs — allows parallel development.
- Tip 2: Version from day one — don’t wait until a breaking change is needed.
- Tip 3: Consumer-driven contract is safer than provider-driven — the consumer knows what it actually needs.
- Tip 4: Review event schema changes with the same rigor as API changes.
- Gotcha 1: Schema evolution is harder than API evolution — all consumers must update before the producer can drop the old format.
- Gotcha 2: Don’t assume ordering guarantees Kafka doesn’t provide (per partition, not global).
- Gotcha 3: Contract tests don’t replace integration tests — they verify the contract, not end-to-end behavior.
- Gotcha 4: Service tokens belong in secrets, not in spec — the spec defines the mechanism, not the values.
The common thread across all eight: contracts are cheap to write and expensive to violate, so front-load the discipline. Version early, keep secrets out, and never assume a guarantee you didn’t specify.
16.15 The Core Insight: Contracts Are Specs for Communication
Just as feature specs define what a service should do, contracts define how services should talk to each other. The same principles apply: they must be explicit and unambiguous, verifiable through contract tests, versioned, and owned — with consumer-driven meaning the consumer owns the contract. When every inter-service communication is governed by explicit, tested contracts, teams can truly deploy independently.
16.16 Summary
SDD in microservice environments requires three types of contract specifications: synchronous HTTP API contracts, asynchronous event contracts, and data/query API contracts.
Consumer-Driven Contract Testing is the most robust approach — consumers define what they need, providers verify they satisfy all consumer needs.
Multi-service specs must cover: service interaction flow, cross-service ACs, event schema with versioning policy, and reliability contracts (what happens when a service is down).
The biggest danger is spec drift — when the event schema in a producer is out of sync with consumer expectations. Automated contract compatibility checks in CI are the first line of defense.
In the next article, we cover Spec Drift Detection — how to detect and prevent code diverging from specification, within a service and across services.