Building in public

Why I still want to understand the apps I build

AI can build faster than I can read. The question is whether I still understand the result well enough to change it.

6 min read

Modular interface panels resolving through logic branches, databases, servers and a system architecture into one path a person at the centre can follow.
Interface, logic, data, infrastructure and system architecture resolving toward a person who can still see the whole structure.

The fastest I have ever produced working software was with an AI assistant writing most of it. The result ran, it looked finished, and for an evening I was pleased with how much ground I had covered. Then I came back a week later to add one feature and found I could not confidently say how the data reached the screen. I could read every part of it. I had not decided any of it.

That week shaped how I build now. I still use AI heavily, because it removes a large amount of work I have no interest in doing by hand. But I choose tools and an approach that keep me able to see the structure of what I am building, because when I lose sight of that, my ability to change the thing goes with it.

What speed hides

A generated application arrives with all of its decisions already made. Somewhere inside it, something chose how state is held, where validation happens, what the data model looks like, which calls run on the client and which on the server, and what the app does when a request fails. Those decisions determine whether the next change takes an hour or a week, and they are the ones a demo never shows.

Generated code is often perfectly reasonable code. The difficulty is that its decisions were made without me, and I discover them only when something forces me to. The first time you notice the data model is wrong for what the product has become, you are not making a small change any more.

Why I build visually

Most of my own work is built in FlutterFlow, a visual builder that sits on Flutter and Firebase. I did not choose it for speed, although for many things it is faster. I chose it for the level of abstraction it puts me at.

In a visual builder the structure is the interface. The widget tree is a tree I can look at. State is a list of variables with names and scopes. An action is a sequence I read from top to bottom. Queries are declared where they are used, so I can see which screen reads which collection and under what condition. When I want to know how something works, I look at it instead of reconstructing it from files I have not opened.

There is a cost. A visual builder constrains what I can express, and now and then I hit something the tool does not want to do. When that happens I write custom code inside it, which leaves me with a small piece of a system I still understand rather than a whole system I do not. For the applications I build, that trade has been worth taking every time.

A team of experienced engineers working in a codebase they know well has no need for my abstraction level, and visual development is not generally better than code. What matters is that there is a level at which one person can still hold a whole system in mind, and I would rather work at that level than above it.

The parts I refuse to leave unread

Whatever the tool, there is a set of things I want to be able to explain about anything I ship. Not written by hand necessarily, but able to be opened, read and predicted:

  • how the interface is structured, and which piece owns which part of the screen;
  • where state lives, when it changes, and what else changes with it;
  • what the data model is, and which screens depend on its shape;
  • what the backend does on its own, in functions and rules, rather than what the app asks it to do;
  • which third-party integrations exist, and what happens when one of them is unavailable;
  • who is allowed to read and write what, expressed as rules rather than as intentions;
  • what the app does when the network is slow, when a write fails, and when a document is missing;
  • what a deployment consists of, and what it changes for somebody using the app at that moment.

These are the ordinary things that decide whether a product survives its second month. The generated version of an app has answers for all of them, and if I cannot state those answers then I am not in a position to maintain it.

Understanding is what makes the second version cheap

Building slightly slower is worth it because almost all the work in a real product happens after the first version.

A change lands easily when you know where it goes. It lands badly when you have to search for the place it belongs, and worse when you find three plausible places. That search is paid for on every change, so a structure you understand pays for itself repeatedly, while a structure you do not understand charges you for the same confusion again and again.

I notice it most in how I test. When I understand the structure I know which three things could break, and I check those. When I do not, I click around the app hoping to trip over the problem, which is neither reliable nor finite.

Where AI fits

I use AI constantly, for a particular kind of work. It is good at the parts that are laborious and reversible: writing a function whose behaviour I can state precisely, producing a first version of a form or a layout I will then adjust, explaining an error or a service's API, or suggesting what I have not thought about in a data model before I commit to it. It turns decisions I have already made into working implementations faster than I would type them.

What I keep is the shape of the system: which decisions the product makes, where the boundaries are, what the data means, what happens on failure, and what the user is promised. Those are the parts I will be answering for later, and the only way to answer for them is to have decided them.

AI can generate, but it cannot take responsibility for what it generated, and it cannot care whether the result is understood. Somebody has to be able to explain the system, test it deliberately, change it safely and accept it. That responsibility does not move just because the typing got faster.

A test I use on myself

I have ended up with one question for my own work. If somebody asked me to change how the app handles a failed payment, or to explain why a screen is slow, could I open the right thing within a minute and say what is happening? When the answer is yes, the speed I gained from AI was real. When it is no, I did not build faster, I only produced faster, and the difference shows up the first time the product has to change.