Skip to main content

Backend layer diagram

The backend follows a layered architecture with clear dependency boundaries. Each layer only talks to the layer directly below it.

Layer responsibilities

Routes

HTTP handlers that parse requests, validate input with Zod schemas, call the appropriate service, and return responses. Routes contain no business logic.

Middleware

Runs before route handlers. Handles:
  • Auth — verifies Cognito JWT tokens, extracts user context
  • Logging — request/response logging with correlation IDs

Services

All business logic lives here. Services orchestrate between repositories, providers, and other services. They are the only layer that makes decisions.

Providers

Data enrichment layer. Providers call external API clients (e.g., SportMonks) and transform raw external data into internal domain formats. This isolates external API quirks from business logic.

Clients

Thin wrappers around external HTTP APIs. Handle request formatting, auth headers, rate limiting, and response parsing. The SportMonks client lives here.

Repositories

CRUD abstractions over DynamoDB tables. Each repository owns one table and exposes typed methods (getById, query, put, delete). No business logic — just data access.

Schemas

Zod schemas for request validation, response shaping, and internal data type definitions. Shared across layers.

Jobs

Background tasks that run on schedules or triggers. Jobs call services — they never access repositories or clients directly.

Dependency injection via factories

The backend uses a factory-based dependency injection pattern defined in factories.ts. Instead of importing concrete implementations directly, each layer receives its dependencies through factory functions.The flow:
  1. factories.ts is the composition root. It instantiates every client, repository, provider, and service with their dependencies.
  2. Each factory function creates an instance and injects the required dependencies via constructor parameters.
  3. Routes and jobs receive fully-wired service instances from the factories.
This means:
  • No layer creates its own dependencies
  • Swapping implementations (e.g., for testing) only requires changing the factory
  • Circular dependencies are caught at startup, not at runtime
  • The full dependency graph is visible in one file
If you add a new service or repository, you must register it in factories.ts. The app will not pick it up automatically.

Rules of thumb

  • Routes never import repositories or clients directly
  • Services never make HTTP calls — that is what clients and providers are for
  • Repositories never contain business logic or call other repositories
  • Jobs always go through services, never bypass them
  • If you are unsure where code belongs, ask: “Does this make a decision?” If yes, it belongs in a service