Spec Kit in a Go Monorepo: One Constitution, Many Services
How to use GitHub Spec Kit in a Golang monorepo. Set up one shared constitution for many services, with per-service configuration and an efficient cross-service workflow.
Running Spec Kit in a monorepo is where Specification-Driven Development meets the messiest real-world layout: one repository holding many services, each with its own domain, tech stack, and team — yet all needing to share the same architectural principles. Spec Kit supports this natively, with a two-level constitution and cross-service feature specs.
In this article we map the monorepo layout, show how the root and service-level constitutions compose, walk through cross-service features and event contracts, and set up a CI strategy that only audits the services that actually changed. The result is consistency across services without losing per-service isolation.
17.1 Monorepo Structure with Spec Kit
The first thing to get right is the directory layout, because it determines how specify resolves configuration and constitutions. The structure below shows a Santekno Shop monorepo with a shared root and per-service .specify/ folders.
1santekno-shop-monorepo/
2├── .specify/ ← Shared: constitution and cross-service features
3│ ├── constitution.md ← Principles for ALL services
4│ ├── .speckit-config.yaml ← Root config (default)
5│ └── features/ ← Cross-service feature specs
6│ └── order-notification/ ← Feature involving multiple services
7│ ├── spec.md
8│ ├── plan-order.md
9│ └── plan-notification.md
10│
11├── services/
12│ ├── order-service/
13│ │ ├── .specify/ ← Service-specific specs
14│ │ │ ├── .speckit-config.yaml ← Config override for order-service
15│ │ │ └── features/
16│ │ │ └── cancel-order/
17│ │ ├── CLAUDE.md ← Service-specific AI context
18│ │ ├── go.mod
19│ │ └── internal/
20│ │
21│ ├── product-service/
22│ │ ├── .specify/
23│ │ ├── CLAUDE.md
24│ │ └── go.mod
25│ │
26│ └── notification-service/
27│ ├── .specify/
28│ ├── CLAUDE.md
29│ └── go.mod
30│
31├── libs/ ← Shared libraries
32│ └── domain-events/
33│ └── go.mod
34│
35└── go.work ← Go workspace fileThe key structural decision is that shared concerns (constitution, cross-service features, contracts) live at the root, while each service owns its own .specify/ and CLAUDE.md. This is what lets one team change their service’s specs without touching another team’s context.
17.2 Constitution Hierarchy: Root vs Service-Level
A monorepo constitution works in two layers: universal rules at the root and service-specific additions below. The root constitution below captures the principles that every service must follow.
1# Root constitution.md — applies to ALL services
2
3## Universal Principles
4- Error code format: UPPERCASE_SNAKE_CASE
5- Monetary values: int64 cents, NEVER float64
6- UUID for primary keys
7- Context propagation: ctx is always the first parameter
8
9## Inter-Service Communication
10- REST for sync calls (with timeouts)
11- Kafka for async events (at-least-once delivery)
12- gRPC for internal service calls needing high performance
13- Circuit breaker REQUIRED for all sync external calls
14
15## API Versioning
16- URL versioning: /api/v1/, /api/v2/
17- Breaking changes require a major version bump
18- Deprecated endpoints: kept for at least 3 months before removalA service then inherits the root and layers on its own rules through config, as shown below for the order service.
1# services/order-service/.specify/.speckit-config.yaml
2# Override for order-service
3
4constitution:
5 # Inherit the root constitution
6 parent: ../../.specify/constitution.md
7 # Service-specific additions
8 additions: |
9 ## Order Service Specific
10 - Order status flow: PENDING -> CONFIRMED -> SHIPPED -> DELIVERED | CANCELLED
11 - Stock reservation: always atomic with order creation
12 - Payment timeout: 15 minutes from order creationThe important principle here is inheritance, not duplication: the service config adds rules but never restates or overrides the universal ones. This keeps the universal principles in exactly one place, so a change to error-code format propagates to every service at once.
17.3 Running Spec Kit in a Monorepo
There are three ways to invoke Spec Kit in a monorepo, depending on where you run it from. The commands below show all three, each resolving the right config and constitution.
1# Option 1: Run from the service directory
2cd services/order-service
3specify feature # uses the config from .specify/.speckit-config.yaml
4 # and inherits the constitution from ../../.specify/constitution.md
5
6# Option 2: Run from the root with a flag
7specify feature --service order-service
8
9# Option 3: Run from the root with a full config path
10specify feature --config services/order-service/.specify/.speckit-config.yamlThe takeaway is that no matter where you launch it, Spec Kit resolves the same two-level constitution — so a developer working inside a service and a CI job running from the root both operate under identical rules.
17.4 Cross-Service Feature Specs
Some features touch more than one service, and Spec Kit models them explicitly. The flow below shows how a single natural-language request becomes a shared spec plus per-service plans.
1# From the root directory
2cd santekno-shop-monorepo
3
4# A feature involving order-service and notification-service
5specify feature --cross-service
6
7# Prompt:
8# "When an order is cancelled, send an email notification to the customer"
9#
10# Spec Kit detects that this involves:
11# - order-service: emits the ORDER_CANCELLED event
12# - notification-service: consumes the event, sends the email
13#
14# Output:
15# .specify/features/order-notification/spec.md ← shared spec
16# .specify/features/order-notification/plan-order-service.md
17# .specify/features/order-notification/plan-notification-service.mdThe elegance is that one shared spec captures the business intent while each service still gets its own plan — coordination and isolation at the same time, so neither team loses ownership of its own implementation.
17.5 Event Contract Specs
In a monorepo, the events flowing between services need to be specified just as carefully as REST APIs. The contract below defines the ORDER_CANCELLED event: its schema, delivery guarantees, and versioning policy.
1# .specify/contracts/order-cancelled-event.md
2
3# Event Contract: ORDER_CANCELLED
4# Producer: order-service
5# Consumers: notification-service, inventory-service
6# Version: 1.0
7
8## Schema (JSON)
9{
10 "event_type": "ORDER_CANCELLED",
11 "version": "1.0",
12 "event_id": "uuid",
13 "occurred_at": "ISO8601",
14 "data": {
15 "order_id": "uuid",
16 "user_id": "uuid",
17 "user_email": "string",
18 "items": [{
19 "product_id": "uuid",
20 "quantity": "int",
21 "refund_amount_cents": "int64"
22 }],
23 "total_refund_cents": "int64",
24 "cancelled_by": "CUSTOMER | ADMIN | SYSTEM",
25 "cancellation_reason": "string | null"
26 }
27}
28
29## Guarantees
30- At-least-once delivery via Kafka
31- Producer: order-service (after a successful DB commit)
32- Partition key: order_id (one partition per order for consumers)
33- Retention: 7 days
34
35## Breaking Changes Policy
36- Additive changes (new optional field): version stays the same
37- Field removal or type change: bump to ORDER_CANCELLED_V2
38- Both versions co-exist for 3 months (migration window)Treating an event contract as a first-class spec is what prevents the classic distributed-systems bug: a producer changes a field and silently breaks three consumers. With the contract written down, that change becomes a visible, reviewable event.
17.6 Service-to-Service Plan Dependencies
When one service’s plan depends on another, that dependency must be explicit — especially the deployment order. The plan below records both the changes and the coordination the order service needs from the notification service.
1# .specify/features/order-notification/plan-order-service.md
2
3## Order Service Changes
4- Add: KafkaOrderCancelledPublisher implementation
5- Modify: CancelOrderUseCase — publish the event after a successful commit
6- Contract: .specify/contracts/order-cancelled-event.md v1.0
7
8## Dependency on Notification Service
9- Notification service must be ready to consume ORDER_CANCELLED before order-service deploys
10- Coordinate deployment order: notification-service FIRST, then order-service
11- Test: publish the event from order-service, verify notification-service consumes itWriting the deployment order into the plan is what turns “the consumer must be ready first” from tribal knowledge into a checked, shared instruction — the kind of detail that, when missed, causes a production incident on release day.
17.7 Monorepo CI: Per-Service Spec Audit
Auditing every service on every PR is wasteful; you only need to audit what changed. The workflow below detects changed services and runs a matrix audit over just those.
1# .github/workflows/monorepo-spec-audit.yml
2
3on:
4 pull_request:
5 paths:
6 - 'services/**'
7 - '.specify/**'
8
9jobs:
10 detect-changed-services:
11 runs-on: ubuntu-latest
12 outputs:
13 services: ${{ steps.detect.outputs.services }}
14 steps:
15 - id: detect
16 run: |
17 # Detect which services have changed
18 CHANGED_SERVICES=$(git diff --name-only origin/main... | \
19 grep '^services/' | \
20 cut -d/ -f2 | sort -u | \
21 jq -R -s -c 'split("\n")[:-1]')
22 echo "services=$CHANGED_SERVICES" >> $GITHUB_OUTPUT
23
24 audit-services:
25 needs: detect-changed-services
26 strategy:
27 matrix:
28 service: ${{ fromJson(needs.detect-changed-services.outputs.services) }}
29 runs-on: ubuntu-latest
30 steps:
31 - uses: actions/checkout@v4
32 - run: npm install -g @github/spec-kit
33 - name: Audit ${{ matrix.service }}
34 env:
35 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
36 run: |
37 cd services/${{ matrix.service }}
38 FEATURE=$(echo "$GITHUB_HEAD_REF" | sed 's/feature\/[A-Z0-9-]*-//')
39 if [ -d ".specify/features/$FEATURE" ]; then
40 specify audit --feature $FEATURE
41 fiThe matrix strategy is what keeps monorepo CI both fast and cheap: a PR touching only the order service audits only the order service, so cost and runtime scale with the change, not with the repository size.
17.8 CLAUDE.md Hierarchy in a Monorepo
Each service should carry its own CLAUDE.md so the AI gets focused, relevant context rather than one giant file. The example below shows the order service’s context, pointing back to the shared root.
1# services/order-service/CLAUDE.md
2
3## Context
4This is the Order Service within the Santekno Shop monorepo.
5Monorepo root: ../../
6Shared constitution: ../../.specify/constitution.md
7
8## This Service
9- Responsibility: Order lifecycle management (create, confirm, ship, deliver, cancel)
10- Team: @order-team (Andi, Budi, Citra)
11- Kafka topics produced: ORDER_CREATED, ORDER_CONFIRMED, ORDER_CANCELLED
12- Kafka topics consumed: PAYMENT_COMPLETED, SHIPPING_UPDATED
13- Database: PostgreSQL (pgx/v5)
14- Other services called: product-service (stock check), payment-service (initiate payment)
15
16## Service-Specific Patterns
17[Patterns unique to order-service that aren't in the global constitution]
18
19## Contracts
20- Produces: ../../.specify/contracts/order-cancelled-event.md
21- Consumes: ../../.specify/contracts/payment-completed-event.mdA per-service CLAUDE.md keeps the AI’s context tight and accurate — it knows this service’s Kafka topics and dependencies without wading through unrelated services, which produces sharper generated code and fewer wrong assumptions.
17.9 Shared Library Spec
Changes to a shared library ripple across every consumer, so they deserve their own spec. The commands below create a feature spec for a library change and flag the downstream impact.
1# Spec for a library change
2cd libs/domain-events
3specify feature --feature "add-review-events"
4
5# Spec at: libs/domain-events/.specify/features/add-review-events/
6# The plan will mention: bumping the library version, all consumers need to updateSpeccing library changes is what makes their blast radius visible before you make them — the plan spells out the version bump and the consumers that must upgrade, so a “small” library tweak never silently breaks five services.
17.10 Monorepo Tips & Gotchas (Quick)
A few principles keep a monorepo Spec Kit setup healthy. The short list below captures the essentials before we dig into the deeper patterns.
Tip 1: Root constitution = universal principles. Service-level constitution = service-specific rules.
Tip 2: Event contracts in .specify/contracts/ prevent schema mismatches between services.
Tip 3: Deployment order must appear in the cross-service plan.
Gotcha 1: Don’t duplicate the constitution — service-level is for additions only, never for overriding the root.
Gotcha 2: A shared-library change can break many services — always spec and plan library changes.
These five rules are the load-bearing ones; the sections that follow expand each into concrete workflows and tooling.
17.11 Cross-Service Consistency at a Glance
Before the deeper patterns, it helps to restate the core model in one line: Spec Kit keeps a monorepo consistent through a constitution hierarchy (root -> service) plus cross-service feature specs, with event contracts in .specify/contracts/ as the glue for inter-service consistency.
That single sentence is the mental model for everything below — every remaining pattern is just an application of “shared where it must be, isolated where it can be.”
17.12 Go Workspace and Spec Kit
A modern Go monorepo uses go.work, and Spec Kit can be made workspace-aware. The setup below shows the workspace file and how specify uses it to resolve cross-service structure.
1# go.work
2go 1.22
3
4use (
5 ./services/order-service
6 ./services/product-service
7 ./services/notification-service
8 ./libs/domain-events
9)
10
11# Spec Kit with workspace awareness
12specify feature --workspace --service order-service
13
14# Spec Kit reads go.work to:
15# - Know every service that exists
16# - Resolve cross-service imports
17# - Generate a plan aware of shared library versionsMaking Spec Kit read go.work means its understanding of the repo matches Go’s own — it sees the same modules the compiler does, so cross-service imports and shared-library versions show up correctly in the generated plan.
17.13 Cross-Service Task Dependencies
For features that span services, tasks.md can reference tasks in other services to encode ordering. The paired task files below show a producer task that depends on a consumer being ready first.
1# .specify/features/order-notification/tasks-order-service.md
2
3## Task 01: Implement the Kafka publisher for ORDER_CANCELLED (order-service)
4**Estimate:** 45 min
5**File:** services/order-service/internal/kafka/order_cancelled_publisher.go
6**Depends on:** notification-service Task 01 (consumer must be ready first for testing)
7
8---
9
10# .specify/features/order-notification/tasks-notification-service.md
11
12## Task 01: Implement the ORDER_CANCELLED consumer (notification-service)
13**Estimate:** 60 min
14**File:** services/notification-service/internal/consumer/order_cancelled_consumer.go
15**Deploy before:** order-service (consumer ready before the producer publishes)These cross-service task dependencies matter for three concrete reasons — coordinating deployment order, sequencing integration tests, and keeping the contract backward-compatible. Encoding them in the tasks means the ordering is enforced by the plan, not remembered by one person.
17.14 Monorepo-Specific Spec Patterns
Some spec patterns only show up in a monorepo, where a single feature legitimately spans producer and consumer services. The two patterns below are the ones you’ll reach for most.
The first is a shared domain-event spec, where one feature explicitly lists the producer and consumer services and splits its ACs accordingly.
1# .specify/features/add-review-event/spec.md
2# This feature touches: product-service (producer), search-service (consumer)
3
4## User Stories
5As a system, when a new review is created, a REVIEW_CREATED event must be published
6so that search-service can update its search index with the latest rating.
7
8## Acceptance Criteria
9### Product Service (Producer)
10- AC1: Publish REVIEW_CREATED after the review is stored in the DB
11- AC2: Event payload matches the contract in .specify/contracts/review-created.md
12- AC3: Publishing is best-effort (don't fail the review if Kafka is down)
13
14### Search Service (Consumer)
15- AC4: Consume REVIEW_CREATED from the product-events topic
16- AC5: Update the Elasticsearch index with the latest rating
17- AC6: Idempotent: processing the same event twice creates no duplicateThe second is a library-change spec, which focuses on backward compatibility and semantic versioning rather than endpoints.
1# libs/domain-events/.specify/features/add-review-event-type/spec.md
2
3## Acceptance Criteria
4- AC1: Add the ReviewCreatedEvent struct to the domain-events package
5- AC2: Backward compatible: don't remove or rename existing types
6- AC3: Version bump: v1.2.3 -> v1.3.0 (minor version, additive change)
7- AC4: Update go.mod in all consumers after the library releaseBoth patterns share the same discipline: name the services involved and split the acceptance criteria by responsibility. That is what lets a cross-service feature stay coherent even though its implementation lands in two or three different codebases.
17.15 Detecting Cross-Service Breaking Changes
The scariest change in a monorepo is one that silently breaks another team’s service. The command below runs a cross-service audit that detects contract-breaking changes and names every affected consumer.
1# Check whether a spec change affects other services
2specify audit --cross-service --feature add-review-event
3
4# Output:
5# Cross-Service Impact Analysis
6#
7# Changed: .specify/contracts/review-created.md
8#
9# Services using this contract:
10# - search-service (consumer, referenced in .specify/features/*/spec.md)
11# - analytics-service (consumer, discovered via Kafka topic config)
12#
13# Breaking change detected?
14# YES: Field 'product_rating_after' type changed: float64 -> int64 cents
15# This is a BREAKING change for all consumers
16#
17# Required actions:
18# 1. Version bump: REVIEW_CREATED_V2 (keep V1 for 3 months)
19# 2. Update the plan for the migration window
20# 3. Notify: @search-team, @analytics-teamAutomated cross-service impact analysis is what turns a hidden landmine into a checklist — the audit names the consumers and even suggests the versioning strategy, so a breaking change becomes a planned migration instead of a surprise outage.
17.16 Monorepo Constitution: The Often-Missed Sections
A monorepo constitution needs sections that a single-service constitution never would. The excerpt below covers the inter-service concerns that are easy to forget until they cause an incident.
1# .specify/constitution.md — Monorepo Section
2
3## Inter-Service Communication Standards
4
5### Synchronous (REST/gRPC)
6- Timeout required: 5 seconds for service-to-service calls
7- Circuit breaker required with thresholds: 50% error rate, 10-second window
8- A service must run in degraded mode when a dependency is down
9
10### Asynchronous (Kafka)
11- At-least-once delivery: consumers must be idempotent
12- Event schema versioning: additive changes okay, field removal/rename = new version
13- Retention: at least 7 days for all topics
14- Dead Letter Queue required for every consumer
15
16### Shared Library
17- Semantic versioning required (semver.org)
18- Breaking changes: major version bump
19- Deprecation notice: at least 1 sprint before removal
20
21## Service Boundaries
22- Every service owns its own database (database per service)
23- NEVER query another service's database directly
24- Data sharing only via APIs or events
25- Circular dependencies between services are strictly forbidden
26
27## Deployment
28- Blue-green deployment for every service
29- Deploy consumers BEFORE producers (consumer-first deployment)
30- A rollback plan is REQUIRED for every breaking changeThese sections encode the hard-won rules of distributed systems — idempotency, database-per-service, consumer-first deployment — so they apply uniformly. Without them in the constitution, each team re-learns the same lessons the hard way.
17.17 Monorepo CI Matrix Strategy
Path-based filtering is the cleanest way to run only the jobs a change requires. The workflow below uses dorny/paths-filter to trigger per-service audits based on which paths changed.
1# Efficient monorepo CI: only audit the services that changed
2
3jobs:
4 detect-changes:
5 runs-on: ubuntu-latest
6 outputs:
7 order: ${{ steps.changes.outputs.order }}
8 product: ${{ steps.changes.outputs.product }}
9 notification: ${{ steps.changes.outputs.notification }}
10 steps:
11 - uses: dorny/paths-filter@v3
12 id: changes
13 with:
14 filters: |
15 order:
16 - 'services/order-service/**'
17 - '.specify/features/**'
18 - '.specify/contracts/**'
19 product:
20 - 'services/product-service/**'
21 notification:
22 - 'services/notification-service/**'
23
24 audit-order:
25 needs: detect-changes
26 if: needs.detect-changes.outputs.order == 'true'
27 runs-on: ubuntu-latest
28 steps:
29 - uses: actions/checkout@v4
30 - run: |
31 cd services/order-service
32 specify audit --all --threshold 85
33
34 # Similar jobs for product-service and notification-serviceNotice that the order service’s filter also watches .specify/contracts/** — a shared contract change correctly re-audits the services that depend on it. Path filtering is what keeps CI both correct and economical as the monorepo grows.
17.18 Monorepo Spec Dashboard
For a bird’s-eye view across all services, Spec Kit renders a monorepo dashboard. The command below surfaces per-service scores and the cross-service checks in one place.
1# Dashboard for the entire monorepo
2specify dashboard --monorepo
3
4# Output:
5# Santekno Shop Monorepo — Spec Dashboard
6# =========================================
7#
8# Services: 3 | Features tracked: 24 | Constitution: v2.1
9#
10# Per-Service Status:
11#
12# order-service 96/100 8 features
13# product-service 89/100 10 features
14# notification-service 82/100 6 features <- needs attention
15#
16# Open Issues:
17# notification-service: 2 features below the 85% threshold
18# - email-template: 78/100 (AC6 missing)
19# - push-notification: 81/100 (EC2 not covered)
20#
21# Cross-Service:
22# Event contracts: 3/3 validated
23# Circular dependency check: OK
24# Constitution compliance: 24/24 featuresA monorepo dashboard makes the weakest service obvious at a glance — here, notification-service with two sub-threshold features is the clear place to focus. It turns “how healthy is the whole system?” into a single, scannable answer.
17.19 Tips & Gotchas for Monorepos
Operating Spec Kit across many services introduces failure modes that don’t exist in a single repo. The tips and gotchas below are the ones worth internalizing.
Tip 1: Version the constitution. Mark the version in constitution.md (# Version: 2.1) and commit it with a tag. A breaking change to the constitution is a major version bump.
Tip 2: Service isolation principle. Every service has its own CLAUDE.md, spec, and plan. Don’t create one CLAUDE.md for all services — it becomes too long and unfocused.
Tip 3: Event contracts are the “API” between services. Treat .specify/contracts/ like an OpenAPI spec for async communication — a contract change is as serious as a breaking REST API change.
Tip 4: Run cross-service audits regularly.
1# Weekly: check consistency across services
2specify audit --cross-service --allGotcha 1: Constitution conflicts across feature branches. If three developers each edit constitution.md in different branches, you get a three-way merge conflict. Process fix: only the tech lead updates the constitution, via a dedicated branch.
Gotcha 2: Cross-service specs can become a bottleneck. If every feature spans multiple services and needs cross-service review, velocity drops. Consider async review for low-risk features.
Gotcha 3: Token cost scales linearly with service count. specify audit --all across 5 services burns roughly 5x the tokens of a single service. Budget accordingly.
Gotcha 4: Go workspace + Spec Kit is still evolving. go.work support in Spec Kit is maturing. For complex workspaces you may need extra custom scripts.
The pattern across these is the same trade-off as everywhere in monorepos: sharing buys consistency but adds coordination cost, so you centralize only what genuinely must be shared and keep everything else isolated per service.
17.20 Summary
Spec Kit in a monorepo uses a two-level hierarchy: a root constitution for universally applicable principles, and service-specific constitutions (or additions) for the rules unique to each service.
Cross-service feature specs in the root .specify/features/ allow coordination between services without losing isolation — each service still owns its own spec, plan, and tasks.
Event contracts in .specify/contracts/ are the “API contract” for async communication between services — a breaking change to a contract must be treated as seriously as a breaking REST API change.
CI strategy for monorepos: path-based filtering ensures only the changed services are audited — efficient in both cost and time.
In the next article, we focus on validating implementation against the specification as a continuous process — not just during debugging, but as an integral part of the development workflow.