DEV Community

Cover image for Choosing the first ten architecture decisions as a founding engineer – which ones compound and which are cheap to reverse
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

Choosing the first ten architecture decisions as a founding engineer – which ones compound and which are cheap to reverse

The situation

Say you are the sole engineering hire at a newly‑funded SaaS startup. The product is a React‑Native mobile client backed by a Node.js 18 API, a PostgreSQL 15 database, and an optional Redis 7 cache. Your co‑founders have just signed a pilot contract with three early customers who will start using the app next week.

You have a two‑week sprint to deliver a functional MVP, but the CTO‑to‑be (your co‑founder) keeps asking whether the choices you make today will survive when you have 10 000 daily active users (DAU) and a $200 k monthly burn budget.

The symptom you see is a growing list of “should‑we‑use X instead of Y?” tickets in the backlog, and the product manager’s warning that any change now will push the launch back by days. The cost is immediate: each indecision adds friction to the sprint, threatens the pilot deadline, and risks losing the first paying customers.

The goal of this guide is to take you from that point of indecision to a concrete set of ten architecture decisions, each annotated with its reversal cost and its potential to compound downstream.


Why it happens

Founding engineers operate under three simultaneous constraints: time‑to‑market, budget, and future‑scale uncertainty. Documentation for each technology (e.g., the Node.js “Cluster” module, Prisma 4 ORM) tells you how to configure it, but it rarely tells you:

  1. Which configuration survives a 10× traffic jump without a full‑scale load test.
  2. What hidden latency or cost appears only under load (e.g., connection‑pool exhaustion in pg‑bouncer).
  3. Which trade‑off is embarrassing to admit (e.g., picking a monolithic repo to speed up early development, then paying the cost of a massive CI pipeline).

Because the startup is cash‑sensitive, you cannot afford to “experiment later” on decisions that lock you into a particular cloud provider, data model, or deployment pattern. Conversely, you also cannot spend weeks building a perfect micro‑service mesh for a product that may pivot. The tension creates a pattern: the first few choices feel cheap to reverse, but later they become the scaffolding on which everything else rests.

Understanding the compound impact (decisions that affect many downstream layers) versus the reversal cost (how much code, configuration, and operational effort you must redo) lets you allocate the limited sprint time to the decisions that truly matter.


The fix, step by step

Below are ten concrete decisions. For each, the step explains what to do now, why it matters, and how to confirm it works. The list is ordered from highest compound impact to lowest, and each entry notes the reversal cost (Low, Medium, High).

# Decision Action Why it matters (compound impact) Reversal cost
1 Choose the deployment target (cloud provider + region) 1. Create a minimal Terraform 1.5 module that provisions a VPC, a single t2.micro (AWS) or e2-micro (GCP) instance, and a PostgreSQL 15 instance with default settings.
2. Deploy with terraform apply -auto-approve and verify the health endpoint (/healthz) returns 200 OK.
The provider determines networking, IAM, and cost model for every later service (CI, monitoring, data storage). Changing provider later forces a full data migration and re‑writes of IaC. High – moving data and re‑authoring IaC can take weeks.
2 Select the API runtime model (single process vs. clustered) 1. In package.json, add a start:prod script that runs node -r ts-node/register src/index.ts for development and node -r ts-node/register src/index.ts --cluster 4 for production.
2. Run node src/index.js --cluster 4 locally and confirm the master logs Cluster started with 4 workers.
The concurrency model dictates how you’ll scale CPU, how you’ll handle graceful shutdown, and which monitoring tools you’ll need. A single‑process app cannot simply be scaled horizontally without rewriting request handling. Medium – you can replace the clustering library, but you must refactor signal handling and session storage.
3 Pick a data access layer (raw SQL vs. ORM) 1. Install Prisma 4 (npm i prisma @prisma/client) and run npx prisma init.
2. Generate the client (npx prisma generate) and add a simple query in src/db.ts that fetches a user by email. Verify with node -e "require('./src/db').getUser('test@example.com').then(console.log)".
The data layer determines schema migrations, query performance, and the shape of future services (e.g., GraphQL vs. REST). Switching from an ORM to raw SQL later requires rewriting all queries and possibly re‑thinking transaction boundaries. High – migration scripts and type definitions need a full audit.
4 Define the API contract (OpenAPI vs. ad‑hoc) 1. Add swagger-ui-express and yamljs (npm i swagger-ui-express yamljs).
2. Create api.yaml with a single /login POST schema and serve it at /docs.
3. Verify by opening http://localhost:3000/docs and confirming the UI loads.
A formal contract locks in request/response shapes that all clients (mobile, web) depend on. An ad‑hoc contract leads to “drift” where each client implements its own version, causing breaking changes later. Medium – you can drop the spec, but you lose generated client SDKs and documentation.
5 Choose stateful vs. stateless session handling 1. Install express-session with a Redis store (npm i express-session connect-redis ioredis).
2. Initialise a Redis container locally (docker run -p 6379:6379 redis:7).
3. Verify a login request sets a connect.sid cookie and that the session persists across two requests.
Stateless JWTs simplify scaling but make token revocation hard; server‑side sessions enable immediate revocation but require a reliable store. The choice affects how you’ll handle load‑balancing and security patches. Low – you can swap JWT for Redis sessions, but you must migrate existing tokens or invalidate them.
6 Select a logging strategy (structured JSON vs. plain text) 1. Add pino (npm i pino) and replace console.log calls with logger.info({reqId}, 'message').
2. Run the app and confirm logs are single‑line JSON objects (e.g., {"level":30,"time":169745...}).
Structured logs enable cheap aggregation in ELK/Datadog later. Switching from plain text to JSON after launch requires a log‑pipeline rewrite and may lose historic readability. Low – replace logger, but you need to re‑run any log‑parsing scripts.
7 Determine CI/CD granularity (monorepo vs. polyrepo) 1. Initialise a single GitHub repository with a /.github/workflows/ci.yml that runs npm ci && npm test on push.
2. Confirm the workflow passes on a PR.
A monorepo speeds up early iteration but can cause CI bottlenecks as the codebase grows; a polyrepo isolates services but adds overhead for cross‑service changes. The decision influences future pipeline cost. Medium – splitting a monorepo later requires moving history and updating build scripts.
8 Pick a feature‑flag system (in‑code vs. external service) 1. Add unleash-client-node (npm i unleash-client-node).
2. Initialise a local Unleash server (docker run -p 4242:4242 unleashorg/unleash-server).
3. Guard a new endpoint with if (await unleash.isEnabled('new‑feature')) { … } and verify the flag works via the UI.
External flag services allow toggling without redeploy; in‑code if (process.env.FEATURE_X) forces a rebuild for every change. The reversal cost is the effort to extract flags from code. Low – you can replace the library, but you must rewrite conditionals.
9 Decide on a static asset pipeline (CDN vs. bundled with server) 1. Add next‑bundle‑analyzer to the React‑Native web build (if you have a web fallback) and configure the output to public/.
2. Deploy the static folder to an S3 bucket with aws s3 sync public/ s3://my‑app‑assets/ and enable CloudFront.
3. Verify a browser fetch returns 200 OK from the CDN URL.
Serving assets from a CDN reduces latency and offloads bandwidth costs; bundling them with the API server works for a tiny MVP but becomes a bottleneck at scale. Low – you can move assets later, but you lose the caching benefits already paid for.
10 Select a monitoring stack (metrics only vs. full APM) 1. Install prom-client (npm i prom-client) and expose /metrics.
2. Run a local Prometheus container (docker run -p 9090:9090 prom/prometheus).
3. Verify that curl http://localhost:3000/metrics returns a # HELP line.
Basic metrics give early visibility; full APM (e.g., Datadog) adds tracing overhead and cost. Adding APM later is possible but requires instrumenting every async boundary. Low – you can add APM later, but you will miss early performance baselines.

How to confirm each step

  • Terraform apply – look for Apply complete! Resources: X added, 0 changed, 0 destroyed.
  • Cluster start – master process logs Worker 1 started for each worker.
  • Prisma query – output should be a JSON object or null without errors.
  • Swagger UI – HTTP 200 and a rendered spec page.
  • Session persistence – Set-Cookie header on login, subsequent request returns same sessionID.
  • Pino logs – each line starts with { and ends with }.
  • GitHub Actions – green checkmark on the PR page.
  • Unleash flag – toggle in UI instantly changes endpoint behaviour without redeploy.
  • CDN asset – curl -I https://d111111abcdef8.cloudfront.net/app.js shows X-Cache: Hit from cloudfront.
  • Prometheus metrics – curl http://localhost:3000/metrics | grep process_cpu_seconds_total returns a number.

By completing these ten steps you lock in the decisions that have the highest downstream impact while keeping the reversal cost low for the rest.


What it costs, and when not to do it

Decision Approx. implementation time (person‑days) Ongoing cost (per month) When to skip or defer
1 – Cloud provider 1.5 $30 (basic VM) If you have a pre‑negotiated cloud credit that forces a provider, you can defer to the credit’s expiry date.
2 – Clustering 0.5 $0 (no extra instances) For a one‑day hackathon you may stay single‑process; accept that you’ll need a rewrite before 100 RPS.
3 – ORM 1.0 $0 (library cost) If your data model is a single table, raw SQL may be quicker; but expect a rewrite if you later need complex joins.
4 – OpenAPI 0.5 $0 If the API is internal‑only and you have a single client, you can postpone formal spec, but plan to add it before the first external partner.
5 – Session store 0.5 $5 (Redis small instance) For a prototype with a single user you can store sessions in memory; you will need to migrate before >100 concurrent users.
6 – Structured logs 0.25 $0–$10 (log aggregation) If you have no log aggregation, plain text is acceptable; add JSON before you exceed 10 GB/month of logs.
7 – Repo strategy 0.5 $0 (GitHub free) If you anticipate >5 services within 3 months, start with polyrepo; otherwise monorepo saves context‑switch cost.
8 – Feature flags 0.5 $0–$20 (Unleash hosted) For a single‑release MVP you can use process.env flags; add a service before you need A/B testing.
9 – CDN 0.5 $0–$15 (S3 + CloudFront low usage) If total asset size < 5 MB, serving from the API is fine; switch when page load exceeds 2 s on 3G.
10 – Monitoring 0.25 $0 (self‑hosted Prometheus) If you have no SLA, basic logs are enough; add metrics before you need to diagnose latency spikes.

When not to do it

  • Provider lock‑in – If you have a strategic partnership that offers free credits for a specific provider, you can postpone the decision until the credits are exhausted.
  • Clustering – When the expected load is < 20 RPS, a single process simplifies debugging; defer clustering until you see CPU > 80 % on a load test.
  • ORM – If you only need CRUD on a single table for the MVP, raw SQL reduces bundle size;

Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.

Top comments (0)