Full Stack Isn't Dead. It Became the Floor.

Producing code in a layer you do not understand is now trivial. Judging that code is exactly as hard as it has ever been.
That gap is the whole change to this job. Not that AI writes code badly, it writes it very well. The difficulty simply moved from one side of the line to the other, and almost every argument about whether engineers are finished is really an argument about people standing on the wrong side of it.
I have been a full stack engineer for most of a decade. This post is the working half of that argument: what judging actually involves, the knowledge it quietly assumes, and the artifact that has replaced code as the thing you are paid to produce. The video below is the same case made as a story, if you would rather hear it than use it.
This is your problem now, whatever your title says
If you write front end code, the backend tickets stopped being somebody else's. If you write backend code, somebody has asked you to fix the spacing on a page recently.
That used to be a career change. Now it's Tuesday.
Front end and back end were separate roles because they demanded genuinely different abilities.
The wall was cost. Learning the other half of the stack was expensive, so we specialised and hired specialists. AI made crossing that line nearly free, which means it was never a wall, it was a price.
So full stack stopped being a specialty and became the floor. The consequence is not that everyone is now full stack. It is that everyone now ships code in layers they never learned, and being handed work in a layer you do not understand is a completely different situation from choosing to learn it.
Which makes review, not authorship, the skill that decides what your output is worth.
What judging actually looks like
Nobody writes this part down, so here is the procedure I run over generated code, especially in a layer I would not have written by hand.
Ask what happens from request to response, in order. If you cannot follow the explanation, you cannot review the code, and that is information about you rather than about the code.
Does this hold a connection, a lock, a thread, or a file handle? For how long? Across a network call? This is the entire category of failure that never appears in the file you are reading.
Is there a query inside a loop? Does it fetch a list and then fetch again per item? Does it look fine only because your test data has fourteen rows?
Dead is easy and usually handled. Slow is what takes you down. Is there a timeout? A retry? Does the retry back off, or does it pour fuel on the fire?
Does the new endpoint verify auth or merely assume it? What lands in the logs? What comes back inside an error message? Generated code is confidently generous in error responses.
A dependency, an environment variable, a new entry in the client bundle. Each is something you now own forever, arriving without the conversation you would have had with a colleague.
Not one of those is a question about syntax. The machine has syntax handled. Every one is a question about the system, and every one assumes knowledge the code itself will not give you. That assumed knowledge is the rest of this post.
The failure that is not in your code
Systems thinking gets taught in the vaguest possible language, so here is the concrete version.
You ship a feature. It is fast. Three months later everything is slow, not just that feature. You read your code and your code is fine: the logic is right, the queries are indexed, nothing you wrote is wrong.
A number somebody picked once, probably a default, almost certainly never revisited.
Ten get a connection. Forty get in line.
They are doing nothing at all, but they still occupy the thing that serves requests.
Now requests that never touch the database are slow too, because there is nothing left to serve them.
Nothing in the code is broken. The system is broken. You will never find it by reading the file where the bug appears to be.
The diagnostic tell is that latency rose on endpoints with nothing in common. A slow query makes one endpoint slow. An exhausted shared resource makes everything slow, including your health check. If your static routes degraded at the same moment as your database routes, stop reading application code and look at what they share. Then check, in this order:
- Pool wait time, not query time. Most pools expose both. Query time can look perfect while wait-for-connection sits at seconds, and averaging them together hides it completely.
- Active versus idle connections at the database. In Postgres,
pg_stat_activityshows what each connection is doing. Anything parked inidle in transactionis the usual culprit: something opened a transaction and then went off to do slow work inside it. - Pool size against real concurrency. Bigger is not automatically better. Every connection costs memory at the database, and a pool larger than the database can serve just relocates the queue somewhere less visible.
- Anything holding a connection across an external call. Fetching from a third party while holding a database connection is how a slow vendor becomes your outage.
The transferable rule is worth more than the fix: when everything degrades together, look for a shared, limited resource. Connection pools, thread pools, file descriptors, memory, a rate limit upstream. The bug lives in the contention, not in the file.
This is one instance of a category you collect one incident at a time, usually at an hour you would rather be asleep. A slow service is more dangerous than a dead one, because the dead one fails immediately and something alerts while the slow one quietly poisons everything upstream. Retries make outages worse, because when a system is struggling the kindest thing you can do is stop asking it for things. Caching solves your problem and creates a new one, which is that exactly one customer is looking at yesterday's data and nobody can reproduce it.
You cannot search for a bug you cannot name
Naming is not vocabulary signalling. It is the difference between a search that finds the answer and one that does not, and it is the difference between an AI fixing your bug and an AI confidently fixing your symptom, because a symptom is all you can describe without the name.
race conditionWorks every time locally, fails occasionally in production, never the same way twice. Two things touched the same state with no guaranteed order. Retrying appears to fix it, which is how it survives review.
N+1 queryFine on test data, unusable on real data, and the slowness scales with row count. One query fetched a list, then the code quietly ran another per item.
hydration mismatchThe page flashes, jumps, or loses interactivity, and the console complains that markup did not match. The server rendered one thing and the client rendered another, usually a date, a random value, or something read off window.
connection pool exhaustionEverything slow at once, including endpoints that touch nothing. Requests are queuing for a shared resource instead of doing work.
thundering herdA cache entry expires and a hundred requests rebuild it simultaneously, so your moment of heaviest load is the moment you have no cache.
memory leakHealthy on deploy, degrading over days, healthy again right after a restart. Something is retained that should have been released, often a listener or an unbounded cache.
Security belongs in the same bucket, treated as somebody else's department right up until the day it is not. I built rotating session tokens for an insurance company, where getting it wrong was a regulatory problem rather than a bug. That kind of work permanently changes how you read code: you stop asking only whether it works, and start asking what it lets somebody do that you did not intend. It is the same shift as the checklist above, applied to a different failure mode.
Why this bites hardest in maintenance
Writing software is the easy part. Keeping it alive is the job.
Dependencies rot, libraries develop holes, frameworks ship breaking changes, and the temporary fix from three years ago is now load bearing with a comment above it reading "do not remove" and no explanation. I have rebuilt an entire codebase from an old component style to a modern one purely to stop it becoming unmaintainable, without adding a single feature.
Underneath all of that sits the hardest problem in this industry: the person who built it leaves, and the reasoning leaves with them. The next engineer inherits code with no context, is frightened to touch anything, and builds around it instead. Every team has a file nobody will open.
Clean architecture, design patterns, code review, tests. None of that was ever academic box ticking. It existed so code could be understood by somebody who was not there when it was written. Keep that purpose in mind, because it is about to be satisfied from an unexpected direction.
The artifact changed
The skill for working with these tools is still widely described as prompt engineering, and the casual version of that is mostly finished. Not useless, just obsolete: the models got good enough that the tricks stopped paying. Role play preambles, offering it a tip, forty page mega prompts stuffed with everything just in case. Modern models follow plain instructions more literally than they follow theatre.
The industry renamed it, and the trail is short enough to check:
- 19 June 2025: Tobi Lütke, CEO of Shopify, preferred "context engineering", describing it as providing all the context for a task to be plausibly solvable by the model.
- 25 June 2025: Andrej Karpathy amplified it as the art of filling the context window with just the right information for the next step.
- July 2025: Gartner published Lead the Shift to Context Engineering as Prompt Engineering Fades, with the line "context engineering is in, and prompt engineering is out".
Simon Willison put the failure best: prompt engineering had been redefined to mean typing prompts full of stupid hacks into a chatbot, and the serious discipline underneath got buried under two years of threads about magic words.
The serious version is not a prompt at all. It is a versioned document in your repository, and there is a standard for it. AGENTS.md was formalised in 2025, donated to the Linux Foundation at the end of that year as a founding project of the Agentic AI Foundation, and is now used by over 60,000 open source projects and read natively by more than 20 coding tools.
Four things belong in it, and none of them are prompts:
Not a diagram. The reasoning. The "why" is precisely what walked out the door when the last engineer left.
Including the ones that fail in a non-obvious way. This single section removes most of the flailing.
What never gets committed, what never ships in a build, which endpoints require auth, what you do and do not log.
What nothing may touch without a human deciding.
Keep it short. Research across more than 2,500 repositories (arXiv 2601.20404) found that past roughly 150 lines you stop getting better results and start paying about 20% more per request for the privilege.
Start at 30 lines. Add a section when the agent consistently gets something wrong. Delete one when the convention changes. Treat it like code, not like documentation.
A starter worth copying, deliberately generic and deliberately short:
# AGENTS.md
## Architecture
App Router frontend, Postgres behind an ORM, background jobs on a worker queue.
Route handlers are thin: validation and auth only. Logic lives in `lib/services`.
Why: handlers became untestable once business logic leaked into them.
## Commands
Install pnpm install
Dev pnpm dev
Test pnpm test -- --run # watch mode never exits in CI
Lint pnpm lint --max-warnings=0
Types pnpm tsc --noEmit
## Security
Never commit or ship: .env, .env._, service account JSON, source maps.
Every /api/admin/_ route requires a session with role=admin. No exceptions.
Do not log request bodies or headers. They carry customer data.
## Boundaries
Do not edit existing files in migrations/. Add a new migration instead.
Do not change auth middleware without a human review.
Do not add a dependency to replace a function we could write in ten lines.Notice how little of that is about AI. It is the document you would want on your first day at a new job, which is the joke buried in all of this. We spent forty years writing documentation nobody read, then stopped writing it because nobody read it. Now there is finally a reader. One that reads every line, every time, without skimming to the code samples, and then does what it says.
Why the fundamentals got harder, not easier
Here is the part that makes all of the above a requirement rather than a nice idea.
You cannot write that document if you do not understand your own system. You cannot describe how a request travels if you have never traced one. You cannot write the security policy if you do not know what you expose. You cannot define boundaries without knowing which parts are load bearing. You certainly cannot write the line about the connection pool if you have never watched one run out.
It used to be possible to be a decent engineer who muddled through, copying patterns without fully understanding why they worked, because you were only ever asked to produce code that ran. Now you are asked to write down the reasoning, and the reasoning is the one thing that does not autocomplete.
The machine will generate the code. It cannot generate the understanding on your behalf, because the understanding is the input, not the output.
So full stack is not dead. It stopped being a job title and became a baseline. It used to mean you could build every layer. It now means you understand every layer well enough to write down how it works, direct a machine building it, and own what ships: the security, the stability, the parts a demo never shows.
The typing is free. The knowing is the job.
And if you have been avoiding the other half of the stack because it was not your area, I have bad news about your next ticket.