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
How factories.ts works
How factories.ts works
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:factories.tsis the composition root. It instantiates every client, repository, provider, and service with their dependencies.- Each factory function creates an instance and injects the required dependencies via constructor parameters.
- Routes and jobs receive fully-wired service instances from the factories.
- 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
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