I think it has never been easier to build and ship a software product. AI has changed how quickly we can explore an idea, turn it into working software, and put it in front of someone who might use it.
That creates an uncomfortable possibility for experienced engineers. Our preference for an academically elegant solution can put us at a competitive disadvantage. While we refine abstractions and prepare for future scale, another team can ship, talk to customers, and learn what to build next.
An architectural decision can be reasonable in isolation and still be the wrong use of time for the product. We need to consider what it postpones. If implementing another layer delays the first useful conversation with a customer, that delay belongs in the calculation too.
I recognise this temptation because I have spent much of my career enjoying exactly those technical questions.
Where my instincts come from
I have always liked learning programming languages. Haskell and Scala interested me because they made me think differently. Functors, monads, effects, the mathematics behind composition: I could spend an evening trying to understand an idea simply because understanding it was satisfying. Learning was a good enough reason on its own.
Naturally, I wanted to bring those ideas into my professional work. There is a particular satisfaction in finding an abstraction that makes a difficult problem fall into place. Once you have experienced that, you start looking for opportunities to do it again.
I have spent fifteen years building software. My work has included backends handling millions of euros every month and a distributed PHP application running on more than 600 Kubernetes pods. Scaling and failure handling were real problems I had to work through. Those experiences shaped my instincts whenever I started a new project.
Domain-Driven Design taught me to find boundaries, develop a shared vocabulary, and keep business rules visible. Event-driven systems taught me about outages, retries, compensation, and processes that succeed only halfway. These were useful things to learn because the systems I worked on gave me concrete reasons to care about them.
The difficulty comes when experience becomes a default answer. Recognising a familiar technical problem feels like understanding the new product. It is easy to start designing for the kind of system you have built before, even when the person in front of you needs something much smaller.
A time tracker, not a platform
I caught myself doing exactly that when my wife asked me to build a time tracking tool.
She felt Toggl had become too bloated for what she needed. She wanted to track time, create reports, and let a few assistants track their work alongside hers. She sent me a comprehensive specification. She had already put effort into explaining the product and the work it needed to support.
And somewhere while reading it, I stopped listening.
I started thinking about which language to use and how to structure the application. I was considering a full event-driven architecture, with asynchronous workflows and plans for scaling. I also started focusing on extra features she had never asked for.
All of those directions were familiar from my career. I knew how to think about them, and I enjoyed thinking about them. Meanwhile, her reason for asking was that the existing tool already did too much. I was expanding the problem before we had built the small solution she wanted.
This is an awkward thing to recognise about yourself. I had a real user, a clear specification, and someone available to explain anything I did not understand. I was still finding ways to move my attention towards the technical decisions I found interesting. The uncertainty I needed to resolve concerned her work. Much of the complexity I was considering concerned a possible future of my own invention.
AI made it easy to pursue any of those ideas. It could explain an architecture convincingly, suggest the next abstraction, and start generating the implementation. A speculative design could become a substantial amount of code before I had properly questioned the reason for building it.
That is one way to consume the advantage these tools give us: spend the time saved on implementation building a more elaborate system.
The trade-offs have changed
Simon Willison describes how coding agents change our trade-offs1. Generating code has become cheaper, while delivering good software still requires substantial work. This should make us reconsider habits built around the assumption that implementing an idea will be expensive.
We can explore a smaller version, see how it behaves, and use what we learn to make the next decision. We have more opportunities to test an assumption through working software. But an agent can also help us implement every possibility we can imagine. We still choose where that capability goes.
Keeping the work focused
For my wife’s tool, I needed to keep bringing the work back to her specification and how she and her assistants would use it. A familiar pattern needed a reason to belong in this product. Being able to implement it quickly was a reason to consider it cheaply, rather than a reason to include it automatically.
We left out the event-driven architecture. The tool is a frontend, an API, and one database. But we still used DDD and a rich domain model. We had a glossary and a harness to guide the agent’s work and check its output. These practices helped us describe the behaviour consistently and keep the domain clear.
That distinction matters to me because I would find it strange to learn all of these ideas and then deliberately avoid the ones that help. DDD gave me a way to discuss the product with AI using consistent terms. I could examine a business rule, see how it appeared in the model, and compare the proposed behaviour with the requirements.
Unmesh Joshi writes about code as a shared model of the problem domain2. His discussion captures why this remains useful with AI: the code contains concepts and vocabulary that people and tools can work with. Types, tests, and clear rules give that vocabulary substance.
For me, the useful question was whether a particular design made the domain easier to understand and change. I could answer that while discussing real behaviour. A preference for a particular architectural style was a weaker reason, especially when its benefits depended on requirements we did not have.
Questioning the agent
Getting there involved an ongoing conversation with AI. I asked it to explain decisions, compare alternatives, and make its assumptions explicit. I questioned its proposals and used my own experience to decide what belonged. I also asked it to challenge my answers, because my first instincts clearly deserved some scrutiny.
Reading the questions coming back was just as important. A question can expose a missing business rule. An answer can settle an ambiguity, commit us to a design, or quietly expand the scope. If I click through those questions just to keep the agent working, I am letting decisions accumulate without giving them enough attention.
That conversation has to move the product forward. Sometimes my experience helps me recognise a useful idea quickly. Sometimes it helps me explain a consequence the agent has overlooked. Sometimes I have to recognise that my own proposal is carrying assumptions from a completely different project.
Five days to production
We went from idea to production in five days. AI wrote 100% of the code, with automated tests covering the behaviour. My wife is using the tool now and is happy with it. It does everything she needs.
I could find things to change if I judged the implementation purely as an architecture exercise. I also have to consider what those changes would improve for her and what else we could do with that time.
The important part is that we reached production and actual use. A finished diagram or a promising local demo would leave different questions unanswered. Once someone uses a product in their work, you have an opportunity to observe whether your understanding survives contact with their day.
The opportunity cost of perfection
For a product aimed at a wider market, that opportunity has a competitive value. Early on, we do not know whether people understand the offer, whether the problem hurts enough, whether the workflow fits, or whether anyone will pay. Those questions matter more than the technical ones occupying our attention.
Suppose two teams have similar ideas. One spends several weeks creating the architecture it wants to maintain for the next five years. The other releases a smaller, understandable version and starts talking to users. By the time the first team ships, the second has already discovered that an important assumption was wrong and changed direction.
The advantage comes from having more opportunities to learn. The second team may improve its product, remove an unnecessary feature, change how a workflow behaves, or discover a more useful problem to solve. Technical preparation alone cannot supply that information.
A decision that postpones feedback can make the codebase more sophisticated while leaving the business less informed. The code may be preparing beautifully for a future the users will never ask for.
AI makes this tension more visible. If a useful first version takes days to build, a week spent debating an abstraction becomes a substantial part of the total delivery effort. We should be able to explain why that week is more valuable than the learning we could get from putting the product into use.
Complexity needs a reason
Martin Fowler made a related argument in Monolith First3, long before coding agents. Separate services carry a management cost, and stable boundaries are difficult to identify at the beginning. His argument connects architecture to the need for early feedback and to what we can realistically know when a product starts.
That underlying problem remains. AI can generate the pieces of a distributed system, but we still have to own the interactions between them. Asynchronous workflows bring questions about ordering, duplicate delivery, retries, and partially completed processes. Those questions may be necessary for a particular product. We should have a clear reason for taking them on.
The same judgment applies to scaling. I start with the expected workload, the consequences of failure, and any requirements we already know we must meet. Further investment waits until usage and operating experience give us better information.
On the payment platform, the load was one of those known requirements from the start, and the distributed architecture earned its place. With my wife’s time tracker, the scale existed only in my head.
A measured bottleneck gives us a specific problem. A workload that needs independent scaling gives us a reason to isolate it. A slow external dependency gives us a reason to move work into the background.
Preparing for an imagined future is much less precise. How many users are we expecting, doing what, and where do we think the system will struggle? If we cannot answer those questions, a more complicated architecture mainly makes us feel prepared.
What is worth protecting
The system also has to remain easy to change. Shipping quickly loses much of its value if every later improvement becomes a struggle. Clear domain boundaries, readable code, and tests around important behaviour support that ability to keep moving.
Readable code matters even more to me when agents generate much of it. I want to find a business rule and understand the path through it without reconstructing a framework of our own invention. I want another person, or another agent, to have enough local context to make a focused change and check its consequences.
Functional programming helps here too. Pure functions make business rules easier to test. Useful types rule out invalid states. Explicit effects make interactions with the outside world easier to follow. These are practical reasons to use ideas that I first enjoyed learning for their mathematical elegance.
Mathematical reasoning gives us confidence in particular properties of a design. The usefulness of the product is a separate question. An abstraction can obey its laws while expressing a business rule we misunderstood, or supporting a workflow nobody wants. We have to test those assumptions with the people doing the work.
There is also a point where a design asks everyone to understand more machinery than the problem requires. I need to recognise that point even when I personally enjoy the machinery. Making something intellectually satisfying for me does not automatically make it a better foundation for the product.
Expertise should move the product forward
I still value the time spent learning difficult subjects. It gives me options and helps me understand their consequences. Professional judgment includes deciding which of those options earns a place now, which can wait, and which solves a problem we do not have.
The decision is about the next useful product step. Will this abstraction make an important change easier? Will it help us understand a business rule or avoid a costly mistake? Does it address a requirement we have, or will it delay the first chance to find out what users need?
There will be plenty of opportunities to apply sophisticated engineering when the product gives us a reason. We do not need to create those reasons ourselves.
AI has made shipping easier. If our attachment to the perfect design keeps us from using that advantage, the expertise we are proud of can become the thing that puts us behind.
References
-
Simon Willison. Writing code is cheap now. Agentic Engineering Patterns, Simon Willison’s Weblog. ↩
-
Unmesh Joshi. What Is Code?. martinfowler.com, 12 May 2026. ↩
-
Martin Fowler. Monolith First. 3 June 2015. ↩
← Back to writing