Skip to content

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., User type extended with membership field)
  • 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