Shape: Microservices
A microservices project has three or more independently deployable services. This is the most complex project shape, and the configuration depends heavily on whether your services live in one repository (monorepo) or separate repositories (polyrepo). This tutorial covers both variants.
When this shape applies
Use the microservices shape when your system has:
- Three or more services that are developed and deployed independently.
- Services that communicate over HTTP, gRPC, message queues, or event streams.
- Services potentially written in different languages.
- Independent scaling requirements per service.
If you have exactly two deployable units (frontend + backend), see Shape: Full-Stack. If everything deploys as one unit, see Shape: Single Repo.
Headless service (backend-only)
A common shape is a pure backend service with no UI — an internal
API, a worker, a CLI binary, or a library exposed over RPC. The init
questionnaire supports this case directly, even though the
platforms[] enum can look frontend-only at first glance.
Configure two fields explicitly when the service ships no UI:
# .gaia/config/project-config.yaml
compliance:
ui_present: false # disables UX, a11y, and design-token gates
platforms:
- server # canonical headless platform identifier
With ui_present: false, the framework correctly skips
/gaia-create-ux,
/gaia-validate-design-a11y,
and /gaia-review-a11y
with a neutral message instead of generating hollow UX artifacts.
With platforms: [server], the
/gaia-config-device-target
gate is satisfied without a mobile / web matrix.
See
/gaia-config-compliance
and
/gaia-config-platform
for the post-init editors.
Monorepo vs polyrepo
The first decision is how to organize your repositories:
| Monorepo | Polyrepo | |
|---|---|---|
| All services in... | One repository | Separate repositories |
| Shared code | Easy -- import from a shared package | Harder -- publish as a library, version independently |
| Atomic changes | One PR can change multiple services | Coordinated PRs across repos |
| CI complexity | Higher -- path filters required | Lower -- each repo has its own CI |
| Team autonomy | Lower -- shared branch protection rules | Higher -- each team owns their repo |
| GAIA config | One project-config.yaml with multiple stacks |
One project-config.yaml per repo, each with one stack |
Choosing between them
Start with a monorepo if your services are tightly coupled, share types or schemas, or are maintained by the same team. Switch to polyrepo when team autonomy becomes more important than code-sharing convenience -- typically when you have 3+ teams working on different services.
Monorepo configuration
In a monorepo, all services live under a shared directory structure
and share a single project-config.yaml. Each service
is a separate stack with its own path.
Folder layout
my-platform/
.gaia/
config/
project-config.yaml
docs/
planning-artifacts/
implementation-artifacts/
services/
user-service/
src/
tests/
Dockerfile
order-service/
src/
tests/
Dockerfile
notification-service/
src/
tests/
Dockerfile
packages/
shared-types/ # Shared code, not a deployable service
src/
tests/
infrastructure/
terraform/
k8s/
README.md
Stacks configuration
# .gaia/config/project-config.yaml
stacks:
- name: user-service
language: go
path: services/user-service
test_command: go test ./...
build_command: docker build -t user-service .
lint_command: golangci-lint run
- name: order-service
language: typescript
framework: nestjs
path: services/order-service
test_command: npm test
build_command: docker build -t order-service .
lint_command: npm run lint
- name: notification-service
language: python
framework: fastapi
path: services/notification-service
test_command: pytest
build_command: docker build -t notification-service .
lint_command: ruff check src/
Each stack has its own language, framework, and commands. GAIA uses
the path field to determine which stack a story belongs
to and which tests to run.
Polyrepo configuration
In a polyrepo, each service has its own repository and its own
project-config.yaml. Each config has a single stack.
Per-repo configuration
# user-service/.gaia/config/project-config.yaml
project:
name: user-service
description: User authentication and profile management
stacks:
- name: api
language: go
path: .
test_command: go test ./...
build_command: docker build -t user-service .
lint_command: golangci-lint run
platforms:
- web
ci_cd:
platform: github-actions
preset: standard
promotion_chain:
- dev
- staging
- production
This looks identical to a single-repo configuration -- because each repository IS a single-repo project from GAIA's perspective. The microservices complexity lives in the interactions between services, not in the per-service configuration.
Coordinating across repos
The challenge with polyrepo is coordination. When a change in the user-service requires a corresponding change in the order-service, you need to:
- Deploy the backward-compatible version of user-service first.
- Deploy the order-service change that depends on the new user-service API.
- Optionally, deploy a cleanup version of user-service that removes the old API.
This is the "expand and contract" pattern. GAIA supports it through
coordinated deployment plans generated by
/gaia-release-plan.
Contract testing
In a microservices architecture, the most common source of production failures is a service changing its API in a way that breaks a consumer. Contract tests catch this before deployment.
What contract testing checks
- The provider (the service exposing the API) still returns the fields that consumers expect.
- The consumer (the service calling the API) still sends requests that the provider can handle.
- Data formats, field names, and types have not changed in breaking ways.
Where contract tests live
In a monorepo, contract tests live alongside integration tests. Each service's test suite includes tests that verify its contracts with other services.
In a polyrepo, contract tests typically use a broker (like Pact Broker) where providers publish their contracts and consumers verify against them.
Configuring contract tests
# .gaia/config/project-config.yaml (monorepo)
test_execution:
tiers:
pr:
command: npm test -- --exclude=contract
timeout_seconds: 120
contract:
command: npm run test:contract
timeout_seconds: 180
nightly:
command: npm test
timeout_seconds: 600
Contract tests are slower than unit tests but faster than E2E tests. Run them on PR (if fast enough) or in a separate CI job that runs after both provider and consumer PRs are merged.
Coordinated deploys
When multiple services need to be deployed together (e.g., a new feature that spans user-service and order-service), coordinate the deployment to avoid downtime:
- Feature flags: Deploy all services with the new code behind a feature flag. Enable the flag once all services are deployed. This is the safest approach.
- Backward-compatible APIs: Deploy the provider first with a new API version that supports both old and new formats. Deploy the consumer next. Clean up the old API version later.
- Blue-green deployment: Deploy all services to a new environment (green), switch traffic from old (blue) to green once all health checks pass.
Use
/gaia-release-plan
to generate a deployment plan that accounts for service dependencies
and ordering.
CI for microservices
Monorepo CI
In a monorepo, enable path filters so each service's tests run only when that service's code changes:
# .gaia/config/project-config.yaml
ci_cd:
platform: github-actions
preset: standard
promotion_chain:
- dev
- staging
- production
path_filters:
enabled: true
concurrency:
group: ci-${{ github.ref }}
cancel_in_progress: true
Running
/gaia-ci-setup
generates one workflow per stack, each triggered by changes to
that stack's path. A change to services/user-service/
triggers only the user-service CI job.
Polyrepo CI
In a polyrepo, each repository has its own CI workflow. No path
filters needed -- the entire repository is one stack. Run
/gaia-ci-setup in each repository independently.
Cross-service integration tests
For both variants, cross-service integration tests (tests that verify interactions between services) should run in a dedicated CI job that is triggered after individual service CIs pass. This is typically a nightly job or a post-merge job on the main branch.
# .gaia/config/project-config.yaml
test_execution:
tiers:
pr:
command: npm test -- --exclude=integration
timeout_seconds: 120
integration:
command: docker-compose up -d && npm run test:integration
timeout_seconds: 300
nightly:
command: npm run test:all
timeout_seconds: 900
Complete monorepo example
# .gaia/config/project-config.yaml
project:
name: ecommerce-platform
description: E-commerce platform with user, order, and notification services
stacks:
- name: user-service
language: go
path: services/user-service
test_command: go test ./...
build_command: docker build -t user-service services/user-service
lint_command: cd services/user-service && golangci-lint run
- name: order-service
language: typescript
framework: nestjs
path: services/order-service
test_command: cd services/order-service && npm test
build_command: docker build -t order-service services/order-service
lint_command: cd services/order-service && npm run lint
- name: notification-service
language: python
framework: fastapi
path: services/notification-service
test_command: cd services/notification-service && pytest
build_command: docker build -t notification-service services/notification-service
lint_command: cd services/notification-service && ruff check src/
- name: web-frontend
language: typescript
framework: react
path: packages/web
test_command: cd packages/web && npm test
build_command: cd packages/web && npm run build
lint_command: cd packages/web && npm run lint
platforms:
- web
- api
ci_cd:
platform: github-actions
preset: standard
promotion_chain:
- dev
- staging
- production
path_filters:
enabled: true
concurrency:
group: ci-${{ github.ref }}
cancel_in_progress: true
triggers:
pull_request:
checks:
- lint
- unit-tests
push_to_dev:
checks:
- smoke-test
- deploy-dev
push_to_staging:
checks:
- smoke-test
- integration-tests
- deploy-staging
push_to_production:
checks:
- smoke-test
- deploy-production
environments:
dev:
url: https://dev.ecommerce.example.com
auto_deploy: true
staging:
url: https://staging.ecommerce.example.com
auto_deploy: true
production:
url: https://ecommerce.example.com
auto_deploy: false
requires_approval: true
test_execution:
timeout_seconds: 300
coverage:
enabled: true
threshold: 75
tiers:
pr:
timeout_seconds: 120
nightly:
timeout_seconds: 900
smoke:
command: npm run test:smoke
timeout_seconds: 60
Complete polyrepo example
In a polyrepo setup, each service repository has a simple config:
# order-service/.gaia/config/project-config.yaml
project:
name: order-service
description: Order processing and fulfillment
stacks:
- name: api
language: typescript
framework: nestjs
path: .
test_command: npm test
build_command: docker build -t order-service .
lint_command: npm run lint
platforms:
- api
ci_cd:
platform: github-actions
preset: standard
promotion_chain:
- dev
- staging
- production
concurrency:
group: ci-${{ github.ref }}
cancel_in_progress: true
environments:
dev:
url: https://order-service.dev.example.com
auto_deploy: true
staging:
url: https://order-service.staging.example.com
auto_deploy: true
production:
url: https://order-service.example.com
auto_deploy: false
requires_approval: true
test_execution:
default_command: npm test
timeout_seconds: 300
coverage:
enabled: true
threshold: 80
Each service is configured independently. The microservices complexity -- contract testing, coordinated deploys, shared infrastructure -- is handled through your deployment process and tooling, not through GAIA configuration.
When to split or merge repositories
Signs you should split a monorepo into polyrepos:
- Different teams own different services and want independent release cycles.
- CI takes too long because all services' tests run on every change (and path filters are not sufficient).
- Access control is needed -- some teams should not be able to modify other teams' services.
Signs you should merge polyrepos into a monorepo:
- Cross-service changes are frequent and coordinating PRs across repos is painful.
- Shared code duplication is growing.
- You spend more time on release coordination than on feature development.
Changing repo structure
Switching between monorepo and polyrepo is a significant undertaking. Make the choice early based on your team structure and expected growth. If in doubt, start with a monorepo -- it is easier to split later than to merge.
What to read next
- Configuring CI Pipelines -- path filters, concurrency, and trigger strategy.
- Test Strategy Configuration -- test tiering and the test pyramid for microservices.
- Environments and Promotion -- coordinating deployments across environments.
/gaia-infra-design-- designing infrastructure for microservices./gaia-release-plan-- planning coordinated releases./gaia-config-stack-- editing stack configuration.- Project Shapes Overview -- comparing all shapes.