ADR-001: Microservices Architecture
Status
Accepted
Context
We are building a platform that needs to support multiple client applications (web, native apps) while maintaining separation of concerns between different business domains (memberships, notifications, content, etc.). The platform must be:
- Scalable: Individual services can scale independently based on load
- Maintainable: Developers can work on services independently without tight coupling
- Observable: We need visibility into request flows across services (see Observability for how this is implemented)
We need to decide on the overall architecture pattern and the specific technologies to implement it.
Decision
We adopt a microservices architecture with the following technology choices:
1. API Gateway: Traefik
We use Traefik as our edge gateway to handle incoming traffic and route requests to internal services.
Rationale:
- Clients talk to a single API endpoint and don't need awareness of our internal topology
- Built-in service discovery, load balancing, and TLS termination
- Native Docker and Kubernetes integration
- Middleware support for authentication, rate limiting, and headers
2. API Layer: Apollo GraphQL Federation
We define services as GraphQL subgraphs and aggregate them using Apollo Federation.
Rationale:
- Single graph for clients: Clients query one schema instead of multiple REST endpoints
- Separation of concerns: Each subgraph owns its domain's data access and business logic
- Composable schema: Subgraphs can extend types from other subgraphs (e.g.,
Usertype extended withmembershipfield) - Schema management: Federation provides tooling for schema composition, validation, and change tracking
- Aggregation: The router can fetch data from multiple subgraphs in a single request
Follow-up ADR
The specific implementation approach for Apollo Federation (e.g., Fastify/Express with Apollo Server vs. NestJS with GraphQL module) will be evaluated in a separate ADR.
Consequences
Positive
- Client simplicity: Single GraphQL endpoint with schema
- Team autonomy: Services can be developed, deployed, and scaled independently
- Schema governance: Federation tooling catches breaking changes before deployment
- Local development: Same code works locally and in production through Docker Compose
Negative
- Operational complexity: More moving parts to deploy and monitor
- Learning curve: Team needs to learn GraphQL Federation, and distributed systems patterns
- Network overhead: Service-to-service calls add latency compared to monolith