← All Engineering Notes

When Building Becomes the Easy Part

AI makes it easier than ever to keep writing code. But moving faster is not the same as building a better system, and sometimes the next useful step is to reconsider the design.

Idea — not yet testedlessonsocraticblock

AISystem DesignDevelopment ProcessVibe Coding

There’s something strangely addictive about building with AI. You have an idea, describe it to a coding agent, and watch it turn into something real. A feature appears, a bug disappears, or a new design comes together. Work that might once have taken an entire afternoon can sometimes be finished in minutes. Before you’ve even had time to appreciate what you’ve built, you’re already thinking about what to do next.

And there’s always something next. Another feature to add, another part of the interface to improve, another idea worth experimenting with. When the distance between thinking of something and seeing it implemented becomes so small, it’s surprisingly easy to keep going. Every completed task feels like progress, and every new possibility makes the project a little more exciting.

I’ve been thinking about this lately because I’ve noticed that same desire in myself. I want to keep pushing code, creating content, improving things, and moving forward. AI makes so much possible that slowing down can almost feel like wasting an opportunity. But I’ve started wondering whether that constant movement is always as productive as it seems. Sometimes I think we become so focused on what we can build next that we forget to look at what we’re building as a whole.

That raises a question I’ve been returning to: are we actually becoming more efficient, or are we simply becoming faster at doing more work? The distinction matters because completing tasks quickly and building the right system are not necessarily the same achievement.

When the next change becomes effortless

Traditional software development has always involved a certain amount of friction. Even a small feature can require understanding the existing code, deciding where the new functionality belongs, writing it, testing the result, and fixing whatever doesn’t work. This effort isn’t necessarily beneficial. Much of it is repetitive, and reducing it gives engineers more time for work that requires judgment. But the effort also creates a cost to implementing an idea, and that cost sometimes gives us a reason to examine it before committing.

AI-assisted development changes this relationship. We can describe what we want in ordinary language, receive a working implementation, and adjust it through conversation. We can explore several possibilities without spending hours writing each version by hand. A developer can prototype an interface, try an unfamiliar library, or investigate an architectural alternative much more easily than before. These are meaningful improvements, especially when we’re still discovering what a project needs.

The interesting problem begins when implementation becomes so accessible that we stop treating it as a decision. Instead of considering whether a feature belongs in the system, we ask the agent to add it. When something doesn’t work quite right, we request another adjustment. If that adjustment introduces a new issue, we fix that too. Each individual change might be reasonable, and the agent may implement every one of them correctly. Yet the project can gradually become more complicated without anyone having made a conscious decision that the additional complexity was worthwhile.

This isn’t a problem unique to AI. Software has always accumulated unnecessary features, duplicated logic, and design decisions that no longer make sense. What’s different is how quickly we can now make changes. When generating another implementation costs relatively little effort, it’s easy to choose another implementation over reconsidering the approach. The friction that once made us hesitate has weakened, but the consequences of our decisions haven’t disappeared.

And that creates an unusual situation. We have more freedom to experiment than ever before, but we also need to become better at recognizing when an experiment has taught us enough to reconsider the design.

A working feature isn’t always a better system

Consider a hypothetical example: someone is building a personal portfolio website. Initially, the requirements are straightforward. Visitors should be able to understand who the person is, explore their projects, read about their experience, and get in touch. A simple website could accomplish all of that, but there are plenty of opportunities to make the experience more interesting.

With an AI coding agent, adding those improvements becomes tempting. The developer introduces animated page transitions, interactive project cards, and a filtering system for browsing different kinds of work. Later, they decide to include an AI assistant that can answer visitors’ questions about the projects. Each feature has a reasonable purpose, and none is necessarily a bad idea. In fact, when designed carefully, they could make the website more useful and memorable.

But imagine that these features are implemented separately over many iterations. The project cards read information from one collection of data, while the AI assistant uses descriptions stored somewhere else. The animation system introduces behavior that the navigation wasn’t originally designed to handle. Some pages become slower because several components request or process information independently. The website still works, and every new feature can be demonstrated, but the overall system has become harder to understand and maintain.

A natural response would be to keep fixing the individual problems. The agent can optimize the slower components, repair the navigation behavior, and add a mechanism to keep the project descriptions synchronized. Those are legitimate engineering tasks, and they might produce real improvements. But they also preserve most of the design decisions that created the problems.

A different approach would be to examine how the features relate to one another. Perhaps the project cards and the AI assistant should use one dependable source of project information. Perhaps page transitions should be designed around the navigation system rather than added independently. Perhaps certain interactions make the website more impressive without making it easier for visitors to understand the work.

These are architectural questions. They concern how the different parts of a system fit together, rather than whether each part functions correctly on its own. When two features maintain separate versions of the same information, for example, the problem isn’t only that they might disagree. It’s that every future update must account for both versions. Fixing the synchronization may work, but removing the unnecessary duplication can eliminate an entire category of future problems.

The example illustrates something that’s easy to overlook during rapid development: a feature can be locally successful while making the larger system less effective. The code might be correct, the tests might pass, and the requested behavior might be present. None of those results automatically tells us whether the feature improves the overall design or the experience of the person using it.

This distinction is particularly important when working with AI agents because they are often given tasks with a narrow objective. If we ask an agent to add filtering to a project gallery, it can focus on implementing the filter. If we ask it to make the gallery faster, it can investigate rendering and data loading. But unless we also examine the wider requirements, we may never ask whether the gallery needs that filtering system in the first place.

The work that doesn’t disappear when code becomes cheaper

One of the most important things to understand about AI-assisted engineering is that producing code and maintaining a successful software system are different kinds of work.

Generating an initial implementation may become much faster, but the implementation still needs to be integrated with everything around it. Its behavior must be verified, its security assumptions need attention, and its dependencies must remain compatible with the rest of the application. Someone also needs to understand how the design should respond when the requirements change. AI can assist with these activities, sometimes substantially, but making the first version easier doesn’t automatically eliminate the work that follows.

This is where unnecessary complexity becomes expensive. Every additional component introduces something that may need to be understood, tested, modified, or eventually replaced. Components also interact, so their combined behavior can be more difficult to reason about than their individual implementations. A change to one part may affect another, even when neither component is particularly complicated in isolation.

Engineers often discuss this in terms of technical debt. A design decision can make today’s implementation easier while increasing the effort required for future changes. Not all technical debt is bad. A quick prototype might deliberately use a temporary solution because the team hasn’t yet established whether the idea is valuable. Spending weeks perfecting an architecture before validating the product could be a much greater waste.

The important distinction is whether we’re making those trade-offs deliberately. A temporary shortcut that helps us learn something can be useful. A shortcut that quietly becomes part of the permanent architecture is a different matter, particularly when new features begin to depend on it. At that point, changing the original decision becomes more difficult because we’re no longer changing one isolated component. We’re changing the assumptions that other parts of the system rely on.

AI can make this process harder to notice. We see the immediate result of each request, but the maintenance burden created by a decision may not become visible until much later. The agent finishes the task, the application runs, and we move forward. Nothing necessarily alerts us that we’ve made the next change more difficult than it needed to be.

This is why I think we need to be careful about what we measure as progress. The number of completed tasks, commits, or generated lines of code can describe activity, but they don’t necessarily describe the quality of the outcome. A simpler architecture that requires less code might represent more meaningful progress than several successful rounds of optimization. Sometimes the greatest improvement comes from discovering that part of the work doesn’t need to exist.

Looking at the whole picture again

When we’re deeply involved in building something, our attention naturally narrows. We think about the component we’re working on, the problem we’re trying to solve, and the next improvement we want to make. That focus is necessary. Software doesn’t get finished through high-level thinking alone, and constantly questioning every decision would make development exhausting.

But there comes a point when looking only at the next task becomes limiting. The architecture may have changed since the project began. New requirements may have made earlier decisions less appropriate. Experiments may have revealed that users need something different from what we initially imagined. If we never revisit the larger design, we risk continuing along a direction simply because we’ve already invested effort in it.

I find it useful to return to three basic questions: Why are we doing this? What are we actually trying to achieve? And how are we approaching it? They’re simple questions, but they examine different things. The first concerns purpose, the second concerns the result we want, and the third concerns the method we’re using to get there.

We often spend most of our development time on the third question. We decide how to implement a feature, which library to use, how to optimize a component, or how to resolve a bug. Those decisions matter, but they become less useful if the first two questions are no longer being answered correctly. An efficient implementation of the wrong requirement is still an inefficient use of effort.

There’s another question that becomes especially valuable as a project develops: if we were designing this system again, knowing everything we’ve learned, would we make the same decisions?

That doesn’t mean we should rebuild everything whenever we discover a better approach. Rewriting a working system can introduce new bugs, consume considerable time, and replace known limitations with unfamiliar ones. An imperfect design may still be entirely appropriate for its purpose. But reconsidering the architecture can show us where the next improvement would actually matter, even when the sensible decision is to keep what we already have.

Sometimes this kind of reflection reveals a technical problem. Perhaps several components should share one data source, or an abstraction has become unnecessarily complicated. Other times it reveals a product problem: we’ve been investing in functionality that doesn’t meaningfully help the people using the software. Those require different solutions, and it’s difficult to distinguish them when our only question is what the coding agent should implement next.

Finding a better rhythm with AI

I don’t think the answer is to slow development down or return to lengthy planning processes before every change. That would sacrifice one of the greatest advantages of working with AI. We can now build experiments quickly, compare alternative implementations, and learn from working software instead of relying entirely on predictions. Sometimes writing a prototype is the best way to discover that our original assumptions were wrong.

What seems more useful is to recognize two different activities within the development process. One is exploration, where we try ideas and learn what works. The other is evaluation, where we use what we’ve learned to reconsider the design. Both create value, but they require different questions. Exploration asks what might be possible. Evaluation asks which possibilities deserve to become part of the system.

We can use AI effectively in both activities. During exploration, an agent might implement several approaches or help build a small experiment. During evaluation, the same agent could examine dependencies, compare architectural alternatives, identify duplicated responsibilities, or challenge assumptions behind a feature. We could ask it to explain what would become simpler if a particular component were removed, rather than immediately asking it to improve that component.

Of course, an AI-generated architectural review isn’t automatically correct. An agent can misunderstand requirements, overlook important context, or recommend an elegant solution that would be unnecessarily expensive to implement. Its analysis needs the same critical evaluation as its code. The benefit is that we can use AI to make engineering reasoning more accessible, not merely to produce implementations faster.

The rhythm doesn’t need to be complicated. We can build, examine the result, learn from it, and then decide what should happen next. After several related changes, it may be worth reviewing how the components now interact. After introducing a major feature, we might reconsider the user journey. When repeated fixes begin appearing in the same area, we can investigate whether the underlying design is responsible rather than treating every symptom independently.

The important part is allowing those observations to change our decisions. A review that produces another long list of improvements without questioning their value can become just another source of tasks. Good reflection should sometimes lead to adding something, sometimes to changing our approach, and sometimes to deciding that the existing system is already sufficient.

Choosing where the speed goes

Something interesting happens when the ability to implement software becomes less of a constraint. We gain more freedom to explore, but deciding where to direct that freedom becomes increasingly important.

Understanding software engineering still matters. Architecture, testing, performance, security, and maintainability don’t become irrelevant because an AI can generate code. In many ways, those concepts become more important because we can now introduce changes so quickly. We need enough understanding to recognize whether the proposed solution is appropriate, what assumptions it depends on, and what consequences it may have elsewhere.

But there’s another skill involved: recognizing which problems are worth solving. That requires understanding the purpose of the system, the people using it, and the difference between adding functionality and creating value. It also requires being willing to reconsider decisions we’ve already made, even when an agent has implemented them successfully.

I think this is where AI-assisted engineering becomes particularly interesting. The technology can help us build much faster, but it can also give us more opportunities to examine our ideas before committing to them. We don’t have to choose between moving quickly and thinking carefully. We can use speed to learn, and then use what we’ve learned to make better decisions.

I’ve been realizing that I don’t always need to push another piece of code or content forward simply because I can. Sometimes the most useful thing is to look at everything that’s already there and understand whether it still fits together, whether it serves the original purpose, and whether there’s a better way to approach the next stage.

There’s a difference between spending less time on a task and needing fewer tasks to reach the same result. AI is very good at helping with the first. The second requires us to keep questioning the work itself.

And perhaps that’s the more interesting opportunity: not simply using AI to build more things, but becoming better at understanding which things are worth building.