12/4/2024
How an obsession with perfect architecture and clean code can prevent startups from finding product-market fit
Written by: Jonathan Haas
The most valuable code I’ve ever written was messy, quick, and written in response to an immediate customer need. Not because it was technically elegant, but because it helped us understand what our customers actually needed. At ThreatKey, some of our most important product insights came from features we shipped in days, not the ones we spent months perfecting.
We’ve all been there. A customer presents a problem, and our engineering instincts kick in. We start thinking about the “right” way to build it - the scalable way, the maintainable way, the way that will handle every edge case. We sketch out architectures, debate interfaces, and plan for a future where this feature needs to support millions of users.
But there’s a fundamental flaw in this thinking. It assumes that we know enough about the problem space to design the perfect solution. In the startup world, this is rarely true.
When we prioritize architectural perfection over customer feedback, we’re not just slowing down development - we’re actively harming our ability to find product-market fit:
I remember two distinct approaches we took to feature development:
The Perfect Approach: We spent months building a beautiful, scalable system for handling complex alert routing logic. The architecture was clean, the code was testable, and the interfaces were elegant. After launch, we learned that customers mostly wanted simple “if this, then that” rules.
The Quick Response: A customer mentioned they needed basic Slack notifications for critical alerts. We hacked together a simple integration in two days - literally hardcoding some webhook URLs. That “temporary” solution revealed exactly how customers wanted to interact with alerts, and the insights informed our entire notification strategy.
After many cycles of this, I’ve developed a new set of principles for startup development:
Ask: “What’s the fastest way we can learn if this is valuable?” Sometimes, that means a manual process behind an API. Sometimes, it means hardcoded values. That’s okay.
Give yourself and your team permission to write code you’ll rewrite. Not because you’re lazy, but because you’re humble enough to admit you don’t have all the answers yet.
Instead of imagining future use cases, wait for customers to tell you what they need:
// Instead of building the perfect abstraction first:
interface NotificationRouter {
route(alert: Alert): Promise<DeliveryResult[]>;
// ... 20 more methods for every possible use case
}
// Start simple and evolve:
async function sendSlackNotification(alert: Alert, webhookUrl: string) {
// 10 lines of code that solve the immediate need
}
The truth is, both the original article and this perspective have merit. The key is knowing when to apply each approach:
In a startup context, technical excellence isn’t about writing perfect code - it’s about:
The next time you’re tempted to spend weeks designing the perfect system, remember that in startups, perfect is the enemy of learning. The goal isn’t to build something perfect - it’s to build something that helps you understand what perfect would even mean for your customers.
Because in the end, the only way to build the right thing is to start by building something and putting it in front of customers. Everything else is just educated guessing.
The real skill in startup engineering isn’t writing perfect code - it’s knowing when good enough is better than perfect.