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 onceTwo 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 yearThirty 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 habitsThis 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:
| Layer | Choice | Why this, at this stage |
|---|---|---|
| Language | The one you know best — default TypeScript | Fluency is what makes reviewing generated code fast and debugging production possible. |
| Application | Next.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. |
| Database | Postgres, 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 access | Drizzle or Prisma | Typed queries and versioned migrations. The migrations matter more than the query builder. |
| Authentication | A 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. |
| Payments | Stripe | Buy it. The webhook-as-source-of-truth pattern in the Airbnb post applies unchanged. |
| Background work | A Postgres-backed queue (pg-boss), or a hosted one (Inngest) | No broker to run until volume demands one. |
| A transactional provider (Postmark, Resend, SES) | Deliverability is a full-time job somebody else already does. | |
| CI/CD | GitHub Actions | Every environment, dev included, deploys from the pipeline. Nobody deploys from a laptop. |
| Observability | Sentry 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 1That 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 wrappersA 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 —
jsonbfor 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
pgvectorextension, 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 eachVercel 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 NoneThat 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: 1Every 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:
| Symptom | What you add | Where 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:
- 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.
- 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
@Serviceclasses”, and “no new datastore or service without a symptom from the graduation table”. That file is how architecture decisions survive thousands of generated lines. - 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.
- 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.
- No manual deployment, not even to dev. A GitHub Actions pipeline from day
one: every push to
mainbuilds, 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. - 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.
- 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.
- 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 prodWhat 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.