Most Engineering Decisions Aren't About Technology
A framework for reasoning about trade-offs, architecture and impact — eight principles I return to on every decision, regardless of which stack is fashionable this year.
Ask ten engineers how to structure a new frontend and you'll get ten different answers about frameworks, rendering strategies and folder conventions. Ask them why, and the conversation gets a lot shorter.
Frameworks rotate. The reasoning underneath doesn't
I've shipped production code across Ember, plain React, Next.js's Pages Router and App Router, three different design systems and at least two waves of “AI will change everything” hype. The tools I used to solve a given problem changed almost every year. The reasoning I used to pick them didn't.
That's not a coincidence — it's the point. A technology choice is downstream of a problem, a set of constraints and a trade-off you're willing to accept. Get good at reasoning about those three things, and you can walk into an unfamiliar stack and still make sound calls on day one. Learn only the current framework's idioms, and you're stuck relearning your judgment every time the ecosystem moves — which, in frontend, is often.
The eight principles below are the ones I actually reach for. None of them are novel. All of them are easy to state and consistently hard to apply under deadline pressure, which is exactly why writing them down is useful.
Software exists to solve problems.
This sounds obvious until you sit in a planning meeting where the debate is Redux vs. Zustand, REST vs. GraphQL for a fetching layer, or monolith vs. microservices, argued with the intensity of a religious dispute and zero reference to what the user or the business actually needs. Technology choices feel like the work. They aren't — they're a means to an end, and the end is always some user or business outcome getting measurably better.
A useful test: can you finish the sentence “this change makes ___ better for ___”? If the blank is a technology (“this makes our codebase more idiomatic”) rather than an outcome (“this makes checkout faster for mobile users”), the change might still be worth doing — paying down debt is real work — but it should be labeled as internal investment, not disguised as user value. Conflating the two is how teams end up rewriting a perfectly functional system for aesthetic reasons and calling it progress.
Before scoping any non-trivial change, write the outcome sentence first. If you can't, that's a signal to find the real problem before writing code — not a reason to skip the step.
Engineering creates value in two ways.
Once you accept that software exists to solve problems, a second question follows immediately: whose problem, and on what timescale? Every piece of engineering work I've shipped falls into one of two buckets. It either makes the product better right now — faster, more reliable, a new capability — or it makes the team better at making the product better, which pays off on every feature that follows.
Teams that only optimize the first bucket ship fast for a quarter and then slow down, because nobody invested in the design system, the test suite or the deploy pipeline that would have kept velocity constant. Teams that only optimize the second bucket build beautiful internal platforms nobody uses, because the product stopped shipping anything users noticed. The job is holding both in tension deliberately, not defaulting to whichever one is easier to justify in a sprint planning meeting.
When planning a quarter, keep a visible ratio between the two buckets — direct product work and “team capacity” work — rather than letting one crowd out the other by default. Zero investment in the second bucket for two consecutive quarters is usually the earliest warning sign of a slowdown three quarters out.
Optimize for outcomes, not technologies.
This is where “don't start with technology” becomes concrete. Take rendering strategy — CSR, SSR, SSG, ISR, streaming, RSC. Every one of those is a legitimate answer to some problem. None of them is a legitimate starting point. The starting point is always: what is the user doing on this screen, and what does that interaction need to feel instant?
I wrote a full breakdown of this exact framework — including the five-step decision process and the case for why CSR is still the right call for manipulation-heavy UIs like Figma or an AG Grid-style table — in Most React Rendering Guides Start With the Wrong Question. The short version: pick the environment that serves the interaction, then let that decision tell you which rendering strategy and which component model to use. Not the other way around.
Every engineering decision is a trade-off.
The rendering-strategy framework above generalizes past rendering. Almost any real engineering decision resolves to the same five-question shape: what's the dominant use case, can the answer be identical for everyone, how often does it change, which parts need continuous interaction, and which environment serves each part best. Swap “rendering strategy” for “database”, “state management library” or “deployment target” and the same five questions still do most of the work.
These are quoted directly from that framework — expand each one to see the answer it points toward:
Consumption optimizes the initial render. Manipulation optimizes the interaction loop.
Yes → SSG or ISR become candidates. No → you need SSR or CSR.
Rarely → SSG. Regularly → ISR. Effectively every request → SSR.
Decided per feature, not per page — some parts of a page can be interactive while the rest isn't.
Server Component by default. Client Component only when the interaction demands it.
Great architecture increases engineering capacity.
Architecture is one of the easiest things to over-invest in for the wrong reason. Elegant layering, perfectly named abstractions and a beautifully documented module boundary are satisfying to build and mean nothing if they don't change what the team can do next. The only question worth asking about an architectural decision is whether it makes the next ten features cheaper to ship than the last ten were.
That reframes a lot of debates. A shared component library isn't good because it's DRY — it's good because it turns a two-day feature into a two-hour one, repeatedly, for every engineer who touches it after you. A monorepo isn't good because it's trendy — it's good if it removes a coordination tax that was slowing releases down. Judge architecture by capacity delta, not by how clean it looks in a diagram.
When evaluating an architecture proposal, ask for the before/after: how long does a representative task take today, and how long will it take once this ships? If nobody can estimate that, the proposal probably isn't ready.
Simplicity scales.
Complexity compounds quietly. Every unmanaged abstraction, special case and workaround left in a codebase doesn't just cost the person who wrote it — it costs everyone who reads that code afterward, until someone pays down the debt. The insidious part is that each individual addition looks cheap in isolation; it's the accumulation that eventually stalls a team, usually right when the business needs velocity most.
Simplicity compounds in the same direction, just positively. Removing a special case, deleting dead code or collapsing three similar components into one reusable primitive doesn't just make the codebase a bit smaller — it makes the next five changes easier to reason about, which makes the five after that easier too. Treat simplification as a compounding investment, not a cleanup pass squeezed into the end of a sprint.
If two engineers, reading the same function independently, describe what it does differently, that function is carrying accidental complexity that's costing the team more than it's saving.
Measure impact, not activity.
Lines of code, PR counts and hours worked are the easiest engineering metrics to collect and the least correlated with whether anything actually got better. They measure motion. A team can look extremely busy by these numbers while shipping nothing that moves user experience, delivery speed or reliability — and a team can look quiet while shipping the one change that unblocks everyone else for a quarter.
The fix isn't to stop measuring — it's to measure the right layer. User-facing metrics (load time, error rate, task completion) and team-facing metrics (implementation time for a representative feature, time to first review, deploy frequency) are harder to collect than a commit count, but they're the only numbers that actually tell you whether the engineering work is working.
Before a project kicks off, agree on the one or two numbers that will define whether it succeeded — written down, not implied — so the retro isn't an argument about how much work happened.
Learn from first principles.
Frameworks, libraries and platforms change fast enough that memorizing their APIs is a depreciating asset. What doesn't depreciate is understanding why a tool exists — what problem its authors were solving, and what trade-off they chose to solve it. That understanding transfers to the next tool almost for free; memorized API surface doesn't transfer at all.
In practice this means reading the “why” before the “how” — an RFC, a design doc, a motivation section — before reaching for the how-to guide. It's slower on day one and dramatically faster on day ninety, because you can predict how an unfamiliar API will behave instead of looking it up every time, and because you can tell when a new tool is solving a problem you actually have versus one you don't.
Conclusion
None of these eight principles are specific to frontend, or to any framework mentioned above. That's deliberate — the point isn't to prescribe a stack, it's to give you a way to evaluate whichever stack you're handed. Technologies will keep changing on a cadence none of us control. The engineers who stay effective through those changes aren't the ones who memorized the most APIs — they're the ones with the clearest reasoning underneath.
For a visual, interactive walkthrough of this same framework, see Engineering Philosophy. For principles 3 and 4 in full depth, see Most React Rendering Guides Start With the Wrong Question.