The Founding Engineer's Tech Stack

October 24, 202520 min readUpdated 10/25/2026

Every post in this track so far has started from a system that already has a problem: too many reads, two guests after one room, a petabyte of video. This one starts from nothing. You have been hired as the first engineer at a company with an idea, a little funding and no code, and someone asks the question that sounds like a hiring-panel warm-up and is actually the most consequential design decision the company will make for two years:

What would you build it with — frontend and backend separate or combined — and where would you host it? And why not the alternatives?

It is a system design question, and it should be answered like one: scope, estimate, then design. The difference is which resource is scarce. Everywhere else in this track the scarce resource has been the machine — CPU, connections, bandwidth, a lock. Here it is the engineer. There is one of you, and with a coding assistant now writing much of the code, what is scarce is not your typing but your attention: reviewing what was generated, deciding what to build, and keeping it running. The risk that kills a company at this stage is not that the system falls over under load; it is that the product ships slowly, or ships code nobody on the team truly understands. Every choice below is judged against that.

The answer this post defends is short enough to say in one breath: the stack the developer already knows, one deployable application, one database, and managed everything else. The rest of the post is why, where each piece earns or loses its place, and — just as important — the exact symptoms that tell you it is time to add the parts the earlier posts in this track describe.

Step 1 — Scope the real requirements

An interviewer wants to see whether you ask what the company needs before choosing anything. The questions worth asking, and the usual answers:

ASK                                         TYPICAL ANSWER AT DAY ONE
  what is the product?                        a web app; a mobile app "later"
  who are the first users?                    hundreds, maybe a few thousand
  how fast must we ship?                      a usable version in 6-10 weeks
  who else is engineering?                    you, then 2-4 hires this year
  regulated data? (health, payments, kids)    usually no; ask anyway
  AI / data-heavy core?                       increasingly often, yes
  what does the team already know well?       the most important answer here

NON-FUNCTIONAL, HONESTLY STATED
  availability   a few minutes down a month is survivable; data loss is not
  scale          small, unknown, and probably wrong if guessed
  cost           low, but engineer-hours cost far more than servers
  change rate    the schema changes weekly; the product pivots at least once

Two lines on that list decide more than all the others. “What does the team already know well” outranks every technical argument in this post, and it matters more now than it used to — Step 4 explains why. “The schema changes weekly” is the other one, because it rules out anything that makes changing your data model expensive — which is the strongest argument against several of the popular wrong answers further down.

Step 2 — Estimate, so the scale argument ends early

The estimation post exists for exactly this moment. Somebody will say “but will it scale?” and the arithmetic is the answer, not a promise:

ASSUME  an optimistic first year: 10,000 daily active users
        50 requests per user per day (page loads, API calls, polling)

        10,000 x 50            = 500,000 requests/day
        500,000 / 86,400 s     ~ 6 requests/second average
        peak at 5x average     ~ 30 requests/second

STORAGE 10,000 users x 5,000 rows each x 1 KB  ~ 50 GB in the first year
        (generous; most products are well under this)

VERDICT one application server handles this with room to spare
        one Postgres instance holds this without partitioning anything
        nothing in the database-scaling post is needed this year

Thirty requests a second is stage two of the build-up in System Design Basics — one application, one database. Saying that out loud, with the arithmetic, shows you know which problems you do not have.

Step 3 — Sort the decisions by how hard they are to undo

Not every stack decision deserves the same amount of thought. The useful question is not “what is best” but “what does it cost to change my mind?” Spend the thinking where reversal is expensive and move quickly everywhere else:

EXPENSIVE TO REVERSE  (think hard, decide once)
  primary database          migrate every row; data never gets cheaper to move
  primary language          cheaper to rewrite than it was, still a full re-review
  data model fundamentals   tenant isolation, ids, money as integers

MODERATE  (decide carefully, migration is weeks not months)
  monolith vs services      merging services back is painful
  auth provider             every user's credentials live there
  frontend framework        a page-by-page rewrite

CHEAP TO REVERSE  (pick something good and move on)
  hosting provider          a container or a build command is portable
  email / SMS vendor        one module wraps it
  CSS framework, ORM        local to the code that uses them
  observability tooling     swap the SDK, keep the habits

This table reframes the question. Hosting, which people argue about most, is near the bottom: moving a container or a framework build between hosts is a routine week of work.

Coding assistants have tilted the table in one direction: anything that is only code got cheaper to redo; data and production state did not. Regenerating a module in another framework is an afternoon. Migrating a live database, retraining a team's instincts, or untangling an outage in a system nobody understands costs exactly what it always did.

Step 4 — The stack, starting with the language you already know

Code is cheap now; understanding it is not

The classic answer to this question picked the “best” language and framework, and the arguments were about writing code: less boilerplate, one language so nobody context-switches, a framework that saves typing. A coding assistant has removed most of that. It writes the boilerplate, it knows the syntax of a framework you have never used, and it switches languages without complaint.

What it has not removed is everything that happens after the code is written. Someone reviews every diff and decides whether it is correct. Someone notices that the generated query runs once per row, that the migration locks a table, that the error path swallows the exception. Someone gets paged when it breaks and has to read a stack trace fast. All of that is judgement, and judgement comes from familiarity. An engineer who knows Django or Spring Boot deeply reviews assistant-written Django or Spring in minutes and catches the subtle mistakes; in an unfamiliar stack the same engineer can only check that the code looks plausible — which is exactly the standard generated code always meets.

So the rule is: use the stack the developer is most fluent in. A fluent Java engineer builds on Spring Boot, a Python engineer on Django or FastAPI, a Rails engineer on Rails, and none of them should switch for this job. Two qualifications keep the rule honest:

  • Keep it mainstream. Assistants are strongest where there is the most public code and documentation. A niche language costs you twice: fewer hires, and a weaker assistant.
  • Cheap code is not cheap complexity. An assistant can generate ten services, four datastores and a Kubernetes manifest in an afternoon. It cannot operate them. Everything the rest of this post argues against is expensive to run, not to write, and that cost did not fall.

When the team has no strong preference, the default below is TypeScript, and the rest of the table holds whichever language you choose:

LayerChoiceWhy this, at this stage
LanguageThe one you know best — default TypeScript Fluency is what makes reviewing generated code fast and debugging production possible.
ApplicationNext.js (or your framework) — frontend and backend in one Pages, server-side rendering and API routes ship as a single deploy. One repository, one pipeline, one thing to roll back.
DatabasePostgres, managed Relational integrity, transactions, constraints, JSONB for the parts of the schema that have not settled, full-text search, and a job queue — one engine doing five jobs.
Data accessDrizzle or Prisma Typed queries and versioned migrations. The migrations matter more than the query builder.
AuthenticationA hosted provider (Clerk, Auth0) or Auth.js Password reset, email verification, OAuth, MFA and session revocation are a month of work you should not do first.
PaymentsStripe Buy it. The webhook-as-source-of-truth pattern in the Airbnb post applies unchanged.
Background workA Postgres-backed queue (pg-boss), or a hosted one (Inngest) No broker to run until volume demands one.
EmailA transactional provider (Postmark, Resend, SES) Deliverability is a full-time job somebody else already does.
CI/CDGitHub Actions Every environment, dev included, deploys from the pipeline. Nobody deploys from a laptop.
ObservabilitySentry plus the host's logs Errors with stack traces and a searchable log are enough until there is real traffic.

Notice what is not there: no Redis, no Elasticsearch, no Kafka, no separate API, no orchestrator. Each has a post in this track, and none is needed until a symptom appears that it cures — the graduation table at the end lists them.

Why TypeScript is the default, and when it is Python

With no preference to honour, TypeScript wins because it is the one language that runs in the browser and is a first-class server language: the API contract becomes a shared type, and breaking it is a compile error rather than a production bug. That guardrail matters more with generated code, not less. If the product's core is AI or data, the libraries live in Python, and the answer becomes a React frontend plus a FastAPI backend — exactly StayHub's shape, still one database and managed hosting.

What the rule looks like for a Java engineer

Take an engineer whose career has been Java and React — this site's author is one. Their day-one stack is not TypeScript at all, and it should not be:

LAYER          DAY ONE, FOR A JAVA + REACT ENGINEER
  backend      Java 21 + Spring Boot, one application
  frontend     React (Vite), built into the same deployable
  database     Postgres, Spring Data JPA, Flyway migrations
  background   Postgres-backed jobs (JobRunr, or a SKIP LOCKED outbox)
  auth         Spring Security as an OAuth2 resource server + hosted provider
  payments     Stripe's Java SDK, the webhook as source of truth
  hosting      one container on Render / Railway / Fly, or AWS App Runner
  datastores   1

That is two languages, and the fluency argument beats the one-language argument. It still ships as one thing: Vite builds the React app into Spring Boot's static resources, so a single jar serves the pages and /api on one domain — no CORS, one deploy to roll back. The seam from Step 5 is Spring's own convention: thin @RestControllers, the logic in @Service classes. Hosting moves from serverless functions to a platform that runs a long-lived JVM container. Everything else in this post is unchanged; the customer data platform post is designed in exactly this stack.

Step 5 — Combined, but with a seam

The “frontend and backend separate or combined?” question has a clear answer at this stage: combined. A separate single-page app and a separate API means two repositories or a monorepo to manage, two deploy pipelines, CORS configuration, a versioning story for the API, and duplicated validation — all cost, paid daily, for a benefit you only collect once a second client exists.

But combined does not mean tangled. The mistake that makes a monolith hard to split later is putting business logic inside route handlers and page components. Keep it in a plain module that knows nothing about HTTP or React, and the web layer becomes a thin adapter over it:

app/                       Next.js: pages, layouts, route handlers  (thin)
  (marketing)/page.tsx
  dashboard/page.tsx
  api/
    orders/route.ts          parses the request, calls domain, shapes the response
    webhooks/stripe/route.ts
domain/                    the actual product. No HTTP, no React, no framework imports
  orders/
    create-order.ts
    order-rules.ts
  billing/
db/
  schema.ts                  tables, in one place
  migrations/
jobs/                      background handlers, called by the queue
lib/                       config, logging, the email and payment wrappers

A sketch of what “thin” means in practice — illustrative, not lifted from a running application:

// domain/orders/create-order.ts — knows nothing about HTTP
export async function createOrder(db: Db, input: NewOrder): Promise<Order> {
  assertValidItems(input.items);
  return db.transaction(async (tx) => {
    const order = await tx.insert(orders).values(toRow(input)).returning();
    await enqueue(tx, "order.created", { orderId: order.id });
    return order;
  });
}

// app/api/orders/route.ts — parse, call, shape. Nothing else.
export async function POST(req: Request) {
  const user = await requireUser(req);
  const input = NewOrderSchema.parse(await req.json());
  const order = await createOrder(db, { ...input, userId: user.id });
  return Response.json(toOrderResponse(order), { status: 201 });
}

The test for whether the seam is real: could a mobile API, a CLI or a job call createOrder without importing anything from app/? If yes, extracting a standalone API later is days, not a rewrite — the option stays open without being paid for.

Notice the enqueue(tx, ...) inside the transaction. That is the transactional outbox from the message queues post, and it is worth having from the first week: the order and the “send a confirmation” job commit together or not at all, with no broker involved.

Step 6 — One database doing five jobs

Postgres is the least controversial line in the stack and the one doing the most work. At this stage it can credibly be:

  • The system of record — transactions and constraints; the concurrency post showed one making a double booking impossible.
  • A document store — jsonb for the part of the model you have not understood yet.
  • A search engine — built-in full-text search with a GIN index covers “search my own records” for a long time.
  • A vector store — the pgvector extension, if the product does retrieval, keeps embeddings next to the rows they describe and inside the same transactions and backups.
  • A job queue — a table, polled with row locks.

The last one sounds like a hack and is not. StayHub's outbox is exactly that, and the claim step is a single query:

    stmt = (
        select(OutboxMessage)
        .where(
            OutboxMessage.status == OutboxStatus.PENDING,
            OutboxMessage.available_at <= datetime.now(UTC),
        )
        .order_by(OutboxMessage.available_at)
        .limit(limit)
        .with_for_update(skip_locked=True)
    )
    return list(db.execute(stmt).scalars().all())

FOR UPDATE SKIP LOCKED lets any number of workers share the table without taking the same row twice, and the queue inherits the database's durability and backups. pg-boss and JobRunr package the same pattern. It stops being enough at sustained high throughput or many independent consumers — that is when the message queues post applies.

Every extra datastore brings the dual-write problem the consistency post spent most of its length on. One database is not a compromise here; it is the design without that problem.

Step 7 — Where it runs

The hosting answer follows from the shape. A single Next.js application and a managed Postgres want a platform that turns a git push into a deploy and requires no infrastructure work at all:

                git push
                   │
                   ▼
   ┌──────────────────────────────────┐     preview URL per pull request
   │  PLATFORM (Vercel / Render /     │ ──> production on merge to main
   │  Railway / Fly)                  │     rollback = redeploy previous build
   │                                  │
   │   CDN ── static assets, pages    │
   │   app ── server rendering, API   │
   │   cron / worker ── jobs          │
   └───────────────┬──────────────────┘
                   │  pooled connections
                   ▼
   ┌──────────────────────────────────┐
   │  MANAGED POSTGRES                │     automated backups, point-in-time
   │  (Neon / Supabase / RDS)         │     restore, branch per preview env
   └──────────────────────────────────┘

   + Stripe, auth provider, email provider, Sentry — all SaaS, all one SDK each

Vercel plus Neon is the lowest-effort version for a Next.js app, with a preview environment and database branch per pull request. Render, Railway or Fly fit a long-running server — Spring Boot, FastAPI, a worker — because they run ordinary containers. Either way the bill is free tiers or tens of dollars a month.

Three things to check before committing to a platform:

  • Connection limits. Serverless functions open database connections per instance, and a traffic spike can open hundreds. Use the provider's pooled connection string from the start, not the direct one.
  • Backups you have restored. “Automated backups” is a claim until you have restored one into a scratch database and run the app against it. Do it in the first month.
  • Region. Put the application and the database in the same region, or every query pays a cross-region round trip.

And when AWS is the right answer

AWS, Google Cloud and Azure are the usual destination, not the start. Move when a concrete need appears — compliance controls, cost at volume, private networking. Hosting is cheap to reverse: a container from Render runs on ECS, and a Neon dump restores into RDS.

Starting there costs weeks: VPC, subnets, IAM, a load balancer, a container service, secrets and infrastructure-as-code, all before the first customer, none of it visible to one.

Step 8 — Build the seams for what you will add later

Deferring a component is only safe if adding it later is cheap. Keep all configuration in one typed settings module, so a new dependency arrives as one new setting. And make optional dependencies genuinely optional — StayHub's cache client treats a missing library and a missing server exactly like a cache miss:

    try:
        import redis
    except ImportError:
        logger.info("redis-py is not installed — the listing cache is disabled")
        return None

That pattern, covered in the caching post, is what makes “add Redis later” a safe sentence: the cache arrives behind an interface that already tolerates its absence.

What the year-two system looks like — StayHub, honestly

It would be dishonest to recommend one database and one application while quoting code from an application that has neither, so here is StayHub's actual shape:

STAYHUB TODAY                              DAY ONE, THE SAME PRODUCT
  guest frontend   React + Vite (SPA)        one Next.js app, or one React SPA
  admin frontend   React + Vite (SPA)          with the admin as a route group
  API              FastAPI                   the same FastAPI, or API routes
  system of record Postgres                  Postgres
  search           Elasticsearch             Postgres full-text + filters
  cache, limiter   Redis                     nothing; add when a read is hot
  GraphQL          Hasura                    nothing; REST is enough
  async work       Postgres outbox           Postgres outbox  (keep this)
  payments         Stripe                    Stripe

  datastores: 4                              datastores: 1

Every line on the left was added to demonstrate a mechanism this track teaches, and each answers a real pressure: search once ranking outgrows SQL, a cache once one page is read thousands of times between edits, a rate limiter once someone scripts the login endpoint. Right components, wrong company — until those problems exist.

The one line identical in both columns is the outbox: reliable async work is needed from the first email, and it costs nothing on the database you already have.

Why the other choices lose at this stage

None of these is a bad technology. Each is a good answer to a problem a founding engineer does not have yet, paid for with the one resource that is actually scarce.

Microservices

Splitting a product into services before it has users turns every function call into a network call that can time out, fail, retry and arrive twice; turns every cross-service change into a coordinated deploy; and replaces a database transaction with a saga. The reasons to split — independent scaling axes, teams blocking each other's releases, genuinely different runtimes — are all organisational or load-driven, and a team of one has none of them. An assistant can scaffold ten services in an afternoon; it cannot carry the pager for them.

Kubernetes, or raw cloud infrastructure on day one

Kubernetes solves running many services across many machines for many teams. With one application it is a large system to operate and secure, for capabilities a platform gives free. Raw AWS costs the same weeks of networking and identity work, before the first customer.

A NoSQL database as the primary store

Nearly every product's data turns out to be relational — users, organisations, subscriptions, invoices, permissions across all of them — and you discover that on the first report someone asks for. DynamoDB also demands your access patterns up front, and in the first year they change every week. Postgres's jsonb gives you schemaless storage where you want it without giving up joins, constraints and ad-hoc queries everywhere else.

Learning a new stack for the job

Picking Go, Rust or Elixir for speed or elegance, when nobody is fluent in them, breaks the familiarity rule: throughput is not the bottleneck, and the assistant will write code you cannot yet judge. If Go is what you know, use Go; just do not learn it on the company's critical path.

Serverless everything

Serverless hosting of one application is fine. Decomposing the application into functions and cloud queues is microservices again, harder to run locally, with more lock-in.

GraphQL

GraphQL earns its complexity when many clients or many teams share one API. With one client written by the person who writes the server, it adds a schema layer, a caching problem and an authorisation surface for no gain. StayHub runs Hasura to teach it, not because it needed it.

When to graduate — the symptoms that justify each part

The discipline that makes the minimal stack safe is being specific about what would change your mind. Every component left out above comes back the moment one of these symptoms appears, and not before:

SymptomWhat you addWhere this track covers it
The same expensive read dominates database load A cache (Redis), cache-aside Caching
Search needs ranking, typo tolerance, facets across millions of rows A search index fed by the outbox Consistency
Reads far outnumber writes and the primary is busy Read replicas, then partitioning Database scaling
Job volume or fan-out outgrows a polled table A real broker or log Message queues
Abuse, scraping or one customer starving the others Rate limiting Rate limiting
A mobile app or public API needs the backend Extract the API from the seam Step 5 above
Teams block each other's releases; one part scales differently Split that part into a service Basics
Compliance, cost at volume, or private networking Move to a major cloud provider Step 7 above

Each addition is triggered by an observation about the running system, never by a forecast. Write the trigger down when you defer a component — “add a cache when the listing query exceeds a fifth of database time” — and the metrics settle it, not taste.

What I would do today, with a coding assistant

Concretely, as a Java and React engineer starting this job now, the first weeks would go like this:

  1. Decide the stack in an hour, not a week. Spring Boot, React, Postgres, one container. The decision is familiarity, so there is nothing to research.
  2. Write the rules down before the first prompt. A project instructions file the assistant reads on every task: the stack and versions, the package layout, Lombok and Flyway conventions, “logic lives in @Service classes”, and “no new datastore or service without a symptom from the graduation table”. That file is how architecture decisions survive thousands of generated lines.
  3. Let it scaffold the skeleton, then review every line of it. Auth, one entity end to end, a migration, a job, a test, a deploy. Everything generated afterwards copies this skeleton's patterns, so its mistakes multiply.
  4. Make tests the contract. Ask for the tests with every feature, and treat a red build as the assistant's problem to fix, not mine.
  5. No manual deployment, not even to dev. A GitHub Actions pipeline from day one: every push to main builds, tests and deploys to dev; production is the same artefact, promoted behind an approval. When the assistant is producing changes faster than anyone could deploy them by hand, the pipeline is the only thing that keeps environments identical and every deploy repeatable.
  6. Delegate the volume, keep the judgement. CRUD screens, forms, DTOs, validation, test cases and docs go to the assistant. The data model, money, permissions, every migration and every production incident stay with me.
  7. Keep diffs small. One feature per change, read before it merges. A two-thousand-line diff gets skimmed, and skimmed code is code nobody understands.
  8. Spend the time saved on users, not on architecture. Cheap code makes it tempting to build the year-two system now. The graduation table still decides.

The pipeline is short enough to write on the first day — a sketch, with the deploy step left to whichever platform you chose:

on:
  push: { branches: [main] }
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: "21" }
      - run: ./mvnw -B verify      # builds React into the jar, runs every test
      - run: ./scripts/deploy.sh dev
  prod:
    needs: build
    environment: production       # requires a reviewer's approval
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh prod

What an interviewer will push on

  • “Won't a monolith fail to scale?” — a stateless monolith scales horizontally behind a load balancer. It cannot scale one part independently, which the estimate says does not matter this year.
  • “Doesn't AI make the stack choice irrelevant?” — it makes typing irrelevant. Reviewing, debugging and operating still depend on fluency, which is why the choice now leans harder on what the team knows.
  • “Isn't Vercel or Render lock-in?” — for hosting, very little: the artefact is a framework build or a container. The lock-in to avoid is in the data layer, which is why the database is plain Postgres.
  • “What would make you change this plan?” — the graduation table. Naming the triggers before you are asked is the strongest signal in the whole answer.

The thread running through this post is the same one that ran through the case studies, turned around. There, the lesson was to add each part at the point it stops being optional. Here, at the very start, the lesson is the converse: every part you add before it is needed is complexity paid for in the one currency a new company cannot spare. Code is cheap now; attention, data and operations are not. Choose the stack you know, keep the seams clean, write down what would change your mind, and spend everything else on the product.

Next, and last: the interview questions — everything in this track, condensed into answers short enough to say out loud.