How we design systems
The principles and defaults we apply when building services at GoodFit. Treat these as the path of least resistance — deviate when there's a good reason and document it.
Monorepo
We work in a monorepo. The main React app lives in the monorepo alongside backend services, so we can share code and types end-to-end and ship full features in a single PR.
Key rule: services may only import from commons modules — never directly from another service. If you're tempted to cross that line, create a shared commons module instead.
See: Code & repos.
Serverless first
- Default: Serverless framework with AWS Lambda, SQS and Step Functions.
- Exception —
appBackend: ECS with Docker, for easier local testing and to avoid Lambda cold-start latency on user-facing traffic.
See: Node / Serverless setup.
Secrets and configuration
- Secrets → AWS Secrets Manager. Reference directly from
serverless.ts. - Non-sensitive config → Systems Manager Parameter Store. Same pattern.
- At runtime → injected as environment variables into the service.
Infrastructure
- Shared infrastructure — defined in CDK in
gf-infrastructure. Used by multiple services. - Service-owned infrastructure — defined in that service's
serverless.ts. - Known exception: crawler ECS infrastructure is structured incorrectly for historic reasons.
See: App Environments.
Storage and state
Pick storage based on data size and access pattern:
- S3 — long-term archive, and temporary working data (uploads, intermediate files). Always attach a retention policy.
- Postgres — small datasets needing fast access, including anything serving interactive requests.
- Redshift — large datasets where 10s+ access latency is acceptable.
Think multi-tenant
Every architectural decision has to account for tenant isolation:
- Consistent experience — one client's behaviour mustn't degrade another's.
- Resource isolation — misconfigured, unexpected, or high-scale usage by one client mustn't impact others.
- Auth on every API — all client APIs must be authenticated, and every call must verify the user has access to the requested record. Shared tables are fine; missing auth checks aren't.
- Use 404 instead of 403 when a user requests a record that exists but belongs to another tenant —
403leaks the existence of the record. - Shared queues are risky — if one client can fill a shared queue, they can block or slow others. Scope per-tenant where possible.
Step Functions for multi-step workflows
Step Functions are the preferred tool for orchestrating multi-step work. They give us good observability and retry semantics for free.
- Scope to a single client where possible (e.g. build dataset, update segment, market preview). This lets workflows run in parallel without cross-tenant impact.
- When to reach for them — if you're designing a system with more than one stage and more than one queue, Step Functions is probably a better fit.
- Distributed Map — use it for large-scale fan-out (hundreds of S3 files, multi-line file processing).
- State size limit — 256KB per execution. For larger payloads, write to S3 (with retention) and pass the S3 URL through state.
Development cycle
Run services locally against shared dev resources where possible:
- Frontend — point the React app at the dev environment for frontend-only changes, or at a local
appBackendExpress instance when backend changes are involved. - Backend (
appBackend) — run locally against dev's Postgres. Apply migrations and deploy WIP to dev whenever you need. - Serverless functions — for simple cases,
sls invoke local -f <function> --stage=dev -d '{}'. For Step Functions or anything stateful, deploy straight to dev withsls deploy --stage=devorsls deploy function -f <function> --stage=dev.
See: Local database and test setup, App Environments, Node / Serverless setup.
Observability
Actively evolving — here's where we are today:
- CloudWatch logs (
console.log) — basic logging. Mind payload size for cost. - New Relic (via the
Loggerhelper) — events and metrics.
See: New Relic.
Security
Defence in depth, multiple layers:
- Network isolation — services deploy into the VPC's private subnet by default. Public-facing ones go through API Gateway or an ECS load balancer.
- Databases — never publicly accessible. Behind VPC firewalls; access via tunnelling. See Accessing databases via port forwarding.
- Security monitoring —
#eng-security-alertscarries Aikido alerts (dependencies, Docker images, AWS misconfigurations). - SQL injection — all SQL must use the
sqlThelper. Never raw strings. See SQL templating. - Input validation —
zodfor parsing and validating user input, wherever practical.
Related
- GoodFit Tech Overview — high-level architecture and stack
- AWS User Access — accessing AWS accounts and CLI setup
- Git Conventions — branch / PR / merge workflow
- Development Lifecycle — how features ship