A junior developer is judged on what they can build. A mid-level developer is judged on what they can build correctly. A senior developer is judged on what the rest of the team can change six months later without being scared.
That last bit is the whole job, and it's the bit that no one tells you about explicitly when you get the title. You don't suddenly start using fancier patterns. You start removing them. You delete code more than you write it. You argue against abstractions that would have impressed your past self. You sit in design reviews and ask "what's the simplest thing that could possibly work" until people get a little tired of hearing it.
The senior job is reducing complexity. Everything else is a side effect.
Accidental Complexity Is The Real Enemy
In Out of the Tar Pit, Moseley and Marks make a distinction that's worth tattooing somewhere visible: there's essential complexity (the problem itself is hard — payments are hard, scheduling is hard, real-time collaboration is hard) and there's accidental complexity (everything else we drag in to solve the essential problem).
The codebase that makes you tired isn't tired because the domain is hard. It's tired because every layer added 10% accidental complexity. The state library that nobody understands. The error wrapper that swallows errors. The custom build step that exists because someone in 2021 needed it. The "framework on top of the framework" that two engineers wrote and then both left. The clever generic util that makes every call site harder to read.
A senior developer's first instinct in any system is to ask: which complexity is essential, and which crept in? You'll find the answer is usually 20% essential, 80% accidental, in any system older than two years. The fix isn't a rewrite. It's the patient, unglamorous work of trimming the accidental layer back, one PR at a time.
Prefer Deletion Over Abstraction
The most senior move in code review is asking "can we just delete this?" Not "can we abstract this?" Not "can we generalize this?" Delete it. Inline the function. Remove the layer. Hardcode the value. Take the indirection out.
This is counterintuitive because every engineering culture rewards abstraction. You learned DRY, you learned design patterns, you learned that duplication is bad. But here's the thing: duplication is cheap and abstraction is expensive. Two copies of similar code can be edited independently. One abstraction shared by ten call sites becomes a load-bearing wall, and any change has to satisfy all ten.
The right rule, taken from Sandi Metz: prefer duplication over the wrong abstraction. You can always extract a function later when the pattern stabilizes. You can rarely undo an abstraction once five teams depend on it.
Deletion is even better than duplication. The best code is the code you didn't write. The second best is the code you removed last quarter.
Delete First, Refactor Second
The instinct to refactor is good. The instinct to delete is better. Most of the "this should be cleaner" code in a five-year-old codebase doesn't need cleaning — it needs to not be there.
Here's the shape of move I make in code review more than any other. Someone has written something like this:
// utils/userFormatter.ts
export const formatUser = (user: User): FormattedUser => {
return {
displayName: user.firstName + ' ' + user.lastName,
initials: getInitials(user),
};
};
// components/UserCard.tsx
import { formatUser } from '@/utils/userFormatter';
const formatted = formatUser(user);
return <div>{formatted.displayName}</div>;
Two files, one import, one indirection, one type alias, one function, one helper for initials — for the sake of producing a string that is "first last." The deletion-first version:
// components/UserCard.tsx
return <div>{user.firstName} {user.lastName}</div>;
Same output. One file. No imports. No new types. The formatUser abstraction was earned only if three or more components actually used it — and even then, it's two lines of duplication, not a load-bearing utility. The "before" version feels cleaner because it has more structure. The "after" version is cleaner because there's nothing to learn.
The same move applies at every scale. A custom hook that wraps one useState. A context provider that holds two booleans. A middleware that adds one header. A service class with one public method that calls one internal method. Look at every layer and ask: would this be clearer if I just inlined it? About 30% of the time, the answer is yes, and the team has been carrying the indirection because nobody was paid to delete it.
The Cost Of Premature Abstraction
A specific kind of pain that compounds for years: someone reads about a pattern, sees a beautiful example of it, and applies it to a problem that doesn't have it yet. Two months later there's a generic EntityRepository<T> that "works for everything," and a year later it's the bottleneck on every feature because every new entity has to negotiate with the constraints the original author imagined the abstraction needed.
I worked on a codebase that had a BaseService class that every domain service extended. It had nine generic methods. Three were used by every subclass. Three were used by exactly one subclass and didn't make sense for the others. Three were used by zero subclasses. The class existed because the original author had read about the Template Method pattern and wanted to apply it.
Removing that class took two weeks of careful refactoring across forty services. The diff was net-negative on lines. Onboarding time for new hires dropped because the service code was now linear: one file per service, no inheritance, no surprises. Nobody missed the abstraction. They had been routing around it for years.
The lesson isn't "don't abstract." It's "abstraction is a debt instrument." You take it out when you have evidence the pattern repeats and stabilizes. You pay it back in maintenance every quarter forever. If you can't write the three call sites that justify the shared layer right now, today, you don't have enough information to design it well. Wait until the third instance and the abstraction will design itself.
Boring Technology Wins
Dan McKinley's "Choose Boring Technology" essay should be required reading at the start of any project. The premise: you have a small budget of "innovation tokens." You can spend them on a wild new database, or a cutting-edge framework, or an experimental build tool — but not all three. Spend them on the part of the system where the business actually differentiates.
This is hard advice for senior engineers because boring is the opposite of impressive. Picking Postgres and Express in 2026 doesn't make a flashy resume. But it makes a system that hires can ramp into in a week, that runs fine for five years without drama, and that doesn't require a tiger team every time someone leaves.
The senior move isn't to know the newest tools. It's to know which tools you can debug at 2 a.m. when the on-call alert is ringing. That list is shorter than it should be, and it gets shorter, not longer, as you get more experienced.
Conway's Law Is Always Watching
Conway's Law: any system you design will end up shaped like the communication structure of the team that built it. People treat this as a curiosity but it's a warning. If your team has three squads, you will end up with three services whether or not the domain wanted three services. If your frontend and backend teams meet weekly, you'll end up with a friendly API. If they meet quarterly, you'll end up with a hostile one held together by frustration.
Skelton and Pais's Team Topologies makes this explicit: organize the teams the way you want the system to be organized. The boundaries you draw between people become the boundaries between modules.
The senior job isn't always to write code. Sometimes it's to notice that the bug you're fixing keeps coming back because two teams own the seam where it lives, and neither of them will fully fix it. That isn't a code problem. That's an org problem dressed up as a bug.
Cognitive Load Is The Real Currency
Team Topologies makes another argument worth internalizing: a team can only handle a fixed amount of cognitive load. If you give a six-person team a system with twelve services, three databases, two languages, and a fragile deploy pipeline, they will be slow and unhappy regardless of their individual talent. The system is over their cognitive budget.
This reframes a lot of architectural debates. "Should we split this into two services?" isn't a technical question — it's a cognitive load question. "Should we adopt this new framework?" isn't a feature comparison — it's a question about how many extra concepts the team has to hold in their head while shipping.
The senior move is to model the cognitive load of every change. The technically correct answer is sometimes the wrong answer if it doubles the team's mental burden for a 5% gain.
Naming Is Half The Architecture
You can tell how senior a codebase is by reading the names. Junior code names things by their type: userArray, dataObject, handleClick2. Mid-level code names things by their structure: OrderRepository, PaymentService, useFormState. Senior code names things by their purpose: Cart, cancelOrder, useCheckoutGuard.
A name does three jobs at once: it tells you what something is, it tells you what it isn't, and it tells you when to use it. A function called processData fails on all three counts. A function called normalizeIncomingWebhook does all three in eight characters of effort.
When code review feels like a slog, the underlying problem is usually names. The reviewer can't hold the file in their head because every identifier requires translation. The fix isn't more comments. It's better names.
I'd rather review 800 lines of well-named code than 200 lines of data, result, temp, helper, manager. The cognitive load of the second is higher, even though it's smaller on disk.
The Questions A Senior Reviewer Actually Asks
Most code review feels like proofreading. The reviewer reads the diff, suggests a rename, points out a missing null check, approves. A senior reviewer reads the same diff and asks a different set of questions. The questions don't appear in the diff — they appear in their head, and they shape the comments they leave.
The three I keep coming back to:
- "Could this be one less file?" New file means new import, new mental model, new place to look when something breaks. Sometimes a new file is right — when the concept is genuinely separate, when the file is huge, when the boundary is meaningful. Often a new file is just procrastinating the decision to put the code where it's used. Ask the question explicitly. If the answer is "it could go inline, but I split it because the function got long," the function probably doesn't need extracting — it needs simplifying.
- "Is this generality earned?" A new utility, a new type parameter, a new config option, a new "for now we'll hardcode this but make it pluggable later." Each one is a small bet that future you will need flexibility. Most of those bets lose. Ask: which call site, today, needs this generality? If the answer is "none, I'm just guessing," push for the specific version. The generic version can come later when there's evidence.
- "Can I name the rule this enforces?" When a piece of code adds a check, a layer, a validation, the senior question is: what invariant is this protecting? If you can't name the invariant in one sentence ("requests to /admin must come from authenticated admins"), the code is probably defensive in a vague way that won't catch the bug it was added to catch. Code without a nameable rule tends to age into superstition — nobody knows why it's there, nobody dares delete it.
A fourth that's specific to JS/TS codebases: "would this fail loudly if it broke?" Silent fallbacks (?? defaults, try/catch around something that should never throw, optional chaining on values that should be required) are a frequent way bugs hide. Errors that surface immediately are cheaper than errors that surface six weeks later in a Slack message from a confused user.
The point isn't to weaponize review. It's to model, out loud, the questions that turn changes from "ships" into "ships and stays good." Junior reviewers learn what to look for by watching senior reviewers ask questions they wouldn't have thought to ask.
What The Job Looks Like In Practice
A senior developer's week looks different from a mid-level developer's week. There's less heads-down implementation and more of the following:
- Refusing scope. The product wanted four features; you negotiate down to two and ship those well. The team's velocity goes up because the half-built features aren't dragging behind.
- Designing the interface before writing the code. A 30-minute conversation about the API contract saves a 3-day rewrite when the consumer hates it.
- Reviewing for what's missing. Junior reviewers catch typos. Senior reviewers ask "what happens when this fails," "who owns this," "how do we delete this later," and "is this the simplest version."
- Writing the dumb version first. The clever one-liner can come in the second PR if it ever earns its keep.
- Killing dead code. Every quarter, search for
TODO, search for feature flags older than six months, search for routes nobody hits. Delete them. Future you will thank present you. - Onboarding well. A senior who can ramp a new hire in three days is worth more than a senior who can write a clever cache layer. The cache layer doesn't compound. The ramp time does.
None of this is "senior because of seniority." It's senior because of effect. Ship outcomes that compound — clearer code, smaller surface area, easier change paths — and the title catches up.
A One-Sentence Mental Model
The senior job is to leave the codebase a little smaller, a little clearer, and a little less brave than you found it — abstractions earn their keep, names do half the architecture, and the best feature is often the one you talked the team out of building.






