Emilio Carrión
Pretty Code Doesn't Pay the Bills (But Pragmatism Does)
Technical purism doesn't pay the bills. Learn when to prioritize speed, when to invest in architecture, and how to use technical debt strategically.
- 1Ownership: The Invisible Superpower
- 2Pretty Code Doesn't Pay the Bills (But Pragmatism Does)
- 3The Senior Engineer Is Dead. Long Live the Expert Generalist
This week I've been reflecting on a misunderstanding I've seen repeat itself throughout my career over and over: confusing pretty code with professionalism. The reality is a bit more complicated than that.
In 60 seconds: Writing perfect code doesn't make you a great professional. Technical purism can be your worst enemy when your startup needs speed. Perfect architecture only makes sense at the right moment in the product lifecycle. What separates a developer from a senior isn't knowing more patterns -- it's knowing when to apply them and when not to.
Recently I built a POC at work. A local React app connected to an API with no tests, no layers, no nothing. I let GitHub Copilot go wild and we had it ready in an afternoon. Tests? None. Architecture? The bare minimum. Abstractions? Zero.
Is that wrong? Yes and no. Because that POC let us show a demo to the business and get the green light for the real project. The next day, that code went straight to the trash. Mission accomplished.
The Myth of Perfect Code
In production, purism doesn't pay the bills. But pragmatism, product vision, and business sense do.
I've seen it in startups, in consulting, in the corporate world where I work now. It's always the same: the people who know the most and contribute the most to the business aren't those who've mastered the most patterns. They're the ones who know when to apply them and when not to.
I've made this mistake myself. In the past, I thought I was the best in the world because I'd learned a ton of cool stuff and wanted to apply it all immediately. The result was building systems that were way too complex for the problem I was solving. Over-architecture. Accidental complexity that slowed the team down instead of speeding it up.
Kent Beck's 3X Framework
Kent Beck proposed a framework that perfectly explains when to apply rigor and when to prioritize speed. It's called 3X and divides the product lifecycle into three phases:
1. Explore
This is the discovery phase. Here we validate the business, reduce time to market, stay ahead of the competition, or learn from our users.
In this phase, perfect architecture can be a waste of resources. If your market needed the solution yesterday, make sure you can move fast. What you want is to generate revenue and start moving your business forward.
If your code prevents you from pivoting, you don't have a masterpiece. You have an anchor dragging you down.
2. Expand
You launched the feature. It has adoption. Now you want to grow rapidly. This is where performance, scale, and growth come into play. The code starts needing structure to support growth.
3. Extract
Mature product that works. Now what you're after is making it maintainable and sustainable over time. This is where you actually need robust architecture, thorough tests, and well-thought-out abstractions.
The Hidden Cost of Premature Abstraction
We've always been told "Don't Repeat Yourself." And it's true for immutable business logic. If you have a rule like "How many columns of boxes fit in a truck?", that should live in one place.
But services, systems, abstractions... many times it's cheaper to duplicate them than to abstract them prematurely.
Why? Because we don't have a clear picture of the problem or the solution yet. Trying to abstract and generalize when we lack clarity is a recipe for disaster.
The Notification Service Example
I remember this perfectly from when I was starting out. I faced the classic notification system. I started by sending emails. Then SMS came along. "Oh," I thought, "I'm going to create a super cool notification service with an abstract interface."
Everything was perfect until Slack arrived. Slack has a block hierarchy, a way of composing messages that my abstract interface couldn't accommodate. I started cramming in Slack-only parameters, exceptions, extra configurations. What I'd built to make my life easier was making it impossible.
The problem isn't code duplication. It's semantic coupling.
If two things change for completely different reasons, they're not the same thing even if the code looks similar. If they have different and timeless reasons to change, don't abstract them. You're dealing with two different concepts.
To decide when to abstract and when to duplicate, use this volatility analysis framework:
Enjoying what you read?
Join other engineers who receive reflections on career, leadership, and technology every week.
Technical Debt as a Strategic Tool
Technical debt is that loan we take from the future to validate hypotheses today.
Imagine you ship code with 200 lines full of if statements. Horrible. But that code lets you land your first 1,000 users. That was a brilliant decision. It let you ship fast, validate, and get the first customers who are going to pay the bills.
The problem is knowing when to decide that code needs to go and be replaced with something more robust.
This decision is what separates a developer from a true senior. The senior doesn't aim for zero debt -- they aim for optimal debt that maximizes delivery while minimizing the cost of iterating forward.
Prototype Code vs. Production Code
Here's how I differentiate them:
Prototype code: That ugly code we wrote quickly to validate something. It's disposable by design.
Production code: The code that will survive over time and let us stay stable in the long run.
Once we've validated the value with prototype code, we transform it into production code. That way we get the benefits of speed without compromising the long term.
The Hidden Cost of Clean Code
On the other hand, there's a real danger in poorly implemented clean code: cognitive load.
If you have magnificent code with 80 layers of indirection, if you need a special architecture just to update a field in the database, you've created accidental complexity.
If a new hire needs a 40-page PDF and an enterprise guide to deliver value, we've failed as architects.
Technical elegance should never be above operational maintainability. Prioritize value delivered to the business, not technical purism.
But make no mistake -- I'm not asking you to be sloppy. I'm asking you to be pragmatic.
Pragmatism vs. Negligence
There's a massive difference between pragmatism and negligence.
Being pragmatic means understanding the tradeoffs, understanding the debt, and knowing how to apply it.
Being negligent means pushing forward without ever looking back. I've seen this in several teams and you end up with months of refactoring, of rebuilding, because what you've built is unmaintainable.
Things That Are Always Worth Doing
There are practices that make sense in every phase:
- Observability: You always want to know what's happening
- Testing: Almost always has a positive cost-benefit ratio
- Don't abstract prematurely: Duplicate code when possible, wait for patterns to emerge
- Track technical debt: Write it down so you don't forget it
When you introduce something that makes you faster now but has a price later, log it. Keep a backlog where you know what you've leveraged and what you need to pay back. If you don't, you'll forget, and there's no coming back from that.
The Key to Balance
We don't get paid to write code. We get paid to solve problems with software sustainably and deliver value to the business.
Best practices and patterns are tools at our disposal. Like any tool, we need to know when to use them.
Context is king. Apply each thing when it makes sense. If you're in the exploration phase, prioritize breaking fast, iterating, learning from the business. If you're in a more advanced phase where you need robustness, that's when it's worth introducing more structured architectures.
Clean code should be a tool, not a dogma.
Question for you: Which phase of the 3X framework is your current project in? Are you applying the right level of rigor, or are you over-architecting?
This content was first sent to my newsletter
Every week I send exclusive reflections, resources, and deep analysis on software engineering, technical leadership, and career development. Don't miss the next one.
Join over 5,000 engineers who already receive exclusive content every week
Related articles
Generating Is Easy. Verifying Is the Work.
Anthropic separated the agent that generates from the one that evaluates and quality skyrocketed. That pattern describes the future of software engineering: generation is commodity. Verification is craft.
How I Built a Digital Twin of València (and Why I Don't Know If It's Art or Engineering)
During Fallas I built València Respira: a real-time visual installation with eleven layers of live data. This article covers the technical and design decisions behind the piece, and what I learned building something that doesn't fit into any category.
The Code Nobody Understands Is Already in Production
AI writes code faster than ever. But there's a trade-off almost nobody talks about: we're exchanging development speed for operational opacity. And that exchange isn't free.
