Engineering philosophy

Principles that survive contact with production.

None of this is theory. Every principle here was paid for by a system that shipped — and a few that shouldn’t have.

Architecture is deciding what stays easy

Architecture has a reputation for being abstract. It isn’t. Architecture is the set of decisions that determine which future changes are cheap and which are expensive. Every codebase has an architecture — the only question is whether someone chose it or whether it accreted.

When I design a system, I’m not drawing boxes for the system we’re building today. I’m placing bets on where change will arrive: which API will be replaced, which feature will double in scope, which team will grow from two engineers to ten. Good architecture is when change arrives where you have a seam waiting for it. Bad architecture is when every change arrives everywhere at once.

This is why I care so much about boundaries — module graphs, protocol seams, SDK facades. A boundary is a promise about what the rest of the system doesn’t need to know. Systems with honest boundaries can be understood one piece at a time, tested one piece at a time, and — most importantly — changed one piece at a time.

A boundary is a promise about what the rest of the system doesn’t need to know.

Performance is a feature users can’t articulate

No user has ever filed a bug that says “the animation dropped to 45 fps” or “cold start crossed 800 milliseconds.” They say the app feels heavy. They say it feels cheap. Then they stop saying anything, because they left.

Performance is the difference between software people use and software people trust. It’s also the most measurable of virtues, which means it’s the one with the fewest excuses. I treat latency, memory, and binary size as budgets, agreed on before the work starts and enforced by CI afterward — the same way a server team treats an SLO. A performance culture isn’t heroic optimization sprints twice a year; it’s never quietly losing ten milliseconds in a code review.

On Apple platforms this discipline is a form of respect. Your code shares a battery, a thermal envelope, and a memory ceiling with everything else a person carries in their pocket. Wasting those is wasting something that belongs to your user, not to you.

Simplicity beats complexity, because complexity compounds

Complexity is a loan. You take it out to ship something sooner, and every engineer who touches the code afterward pays the interest — in onboarding time, in bugs that hide in interactions, in features that take a month instead of a week. Most struggling codebases aren’t struggling because someone wrote bad code. They’re struggling because a hundred small clevernesses compounded.

So my default is aggressive plainness. Value types over clever object graphs. One obvious way to do a thing instead of three flexible ways. Frameworks earn their way in; they’re never invited by default. If a junior engineer can’t explain the flow of data after a week, that’s not their failure — it’s an architectural code smell.

The hard part is that simplicity is not the same as ease. Simple takes longer to design. It means saying no to abstractions that would be fun to build, and deleting code that was hard to write. Simplicity is a spending decision about other people’s future attention — and the budget is smaller than anyone thinks.

Complexity is a debt every future engineer pays. My job is to keep the system simple enough that the next decision is easy.

The next engineer is the primary user

Every line of code has two audiences: a compiler that reads it once, and a colleague who reads it for years. We optimize obsessively for the wrong one. The compiler doesn’t care about your names, your module boundaries, or whether the error message explains itself at 2 a.m. — the next engineer cares about nothing else.

So I design for a specific person: someone competent, tired, under deadline, with none of my context, opening this code for the first time eighteen months from now. What would they need? Names that say what things are. A data flow that reads in one direction. Errors that state whose fault they are. Decision records that explain not just what we chose but what we rejected, and why. Every one of those is cheap on the day it’s written and priceless on the day it’s needed.

This isn’t altruism — it’s self-interest with a delay. The next engineer is usually you, minus the context you’re sure you’ll remember. Half my best architectural habits are just apologies to a version of myself who once spent a weekend rediscovering his own reasoning.

Mentoring is architecture for teams

The systems I’m proudest of aren’t the ones I built; they’re the ones that kept getting better after I stopped touching them. That doesn’t happen because the code was good. It happens because the people around the code understood why it was shaped that way and felt ownership of the shape.

So I treat mentoring as an engineering activity with engineering outputs. Design reviews where the reasoning is spoken out loud, not just the verdict. Pull-request feedback that teaches the principle, not just the fix. Documents that record why we didn’t do things, because the rejected options are where the real lessons live. A team that understands its own architecture is the only mechanism that keeps an architecture alive.

The measure of a senior engineer isn’t the hardest problem they can solve. It’s how many problems stop being hard for the people around them.

Software should evolve like a city, not a monument

Monuments are finished. Cities are alive — and cities survive because they change incrementally, under rules that protect what matters while everything else stays negotiable. That’s the model I hold for long-lived software.

Concretely, it means I optimize for reversibility. Migrations over rewrites, always: the strangler pattern, feature flags, versioned operations, deprecation windows measured honestly. It means the system’s invariants — the things that must never break — are written down, tested, and few. And it means treating deletion as a first-class engineering achievement. Code you removed can’t regress, can’t be exploited, and can’t confuse anyone at 2 a.m.

Five years is my design horizon. Not because I can predict five years of product decisions — nobody can — but because a system designed to absorb five years of unpredicted decisions looks fundamentally different from one designed to demo well next quarter. It has seams. It has budgets. It has fewer ideas, held more firmly.

That’s the software I want my name on: still simple, still fast, still trusted — long after the launch tweet is forgotten.

“The measure of a senior engineer isn’t the hardest problem they can solve. It’s how many problems stop being hard for the people around them.”
See the principles in practice