I've been writing production code since 2008. That's not something I say to impress anyone — it's just context for what follows. Over those 16 years, I've built payroll systems for Ethiopian government agencies, redesigned the UN's humanitarian affairs website, connected 2.1 million schools to real-time connectivity data, and migrated 20+ enterprise systems to Azure. Different domains, different continents, wildly different constraints.
What follows are observations — not rules. They're the things I notice I actually believe, now that I've had enough time to see patterns repeat.
Shipping is the skill
The first lesson, and the one I keep relearning: the ability to ship is a skill in itself, separate from the ability to code well. Some of the most technically talented engineers I've worked with were terrible at shipping. They would endlessly refine, re-architect, add abstraction layers that "would be useful later." Later rarely came.
Shipping doesn't mean cutting corners. It means making a concrete decision about what "good enough" looks like for this context, right now, and executing on it. The next version will be better because you'll know things you can't know until it's running in production with real users making real decisions with it.
The gap between what you imagine your software does and what it actually does collapses the moment it hits production.
Your users will always surprise you
Whether it was UN agency staff in New York or school administrators in Kenya, users consistently do things with your software that you never imagined. This sounds obvious, but it has a practical consequence: don't build for the user you assume you have. Build for the user who will find your system three years from now, in a context you couldn't predict.
At UNICEF's Project Connect, we built REST APIs that ended up being consumed by policy groups, economists, and journalists — not just internal technical teams. We hadn't fully anticipated those users. But because we took API-first design seriously — proper documentation, stable contracts, predictable versioning — we could accommodate them without emergency refactors. Good default decisions compound.
Boring technology is underrated
I've used many frameworks, platforms, and tools over 16 years. The things I reach for most are rarely the newest. C#, .NET Core, SQL, REST APIs. Not because they're exciting, but because they're boring in the right way — predictable, well-documented, with solutions to most problems already written and indexed somewhere.
The geeky part of my brain is always interested in new tools. The part responsible for shipping production systems that governments and NGOs depend on reaches for boring, stable, proven technology. There's a time and place for experimentation — but that time is not when you're migrating mission-critical enterprise systems.
# The stack I keep reaching for
C# / .NET Core — predictable, performant, enterprise-grade
Azure Functions — serverless without the sharp edges
SQL Server — boring and reliable
REST + OpenAPI — universal, well-understood contracts
Pick boring where it matters. Experiment on the edges.
What hasn't changed
I started my career building desktop Windows apps in 2008. Since then, the industry has moved through cloud, mobile, microservices, containers, serverless, and now AI. Despite all of it, the core problems remain identical:
- Understanding what the user actually needs (not what they said they need)
- Decomposing a complex problem into pieces teams can actually build
- Communicating clearly with people who don't think in systems
- Resisting the urge to over-engineer for requirements you don't have yet
The tools change. The problems don't. This is either reassuring or frustrating depending on the day.
On AI and what's next
I've spent the last few years integrating AI into enterprise systems — Azure Cognitive Services, generative AI pipelines, intelligent document processing. The capability jump is real. I'm not a skeptic.
But I'm wary of a specific failure mode I'm starting to see: engineers using AI to ship bad code faster. The leverage is genuine, but so is the risk of losing fluency with the fundamentals that make systems maintainable and trustworthy over time. If you can't read and reason about the code your AI assistant generates, you don't own it — you're just a conduit.
Use the tools. Stay curious. Understand what's happening underneath.
This is a first post on what I hope will be a regular space for thinking out loud about software, architecture, and building things that outlast the hype cycle. Not trying to be authoritative — these are working notes from 16 years of stumbling through the industry.
More soon.