Order-Driven Development
The other day, I posted about an article title Use Your Type System. Despite firmly believing that most Ruby applications do not need and should not adopt the static type systems on offer, I’m not anti-static types. I really like static types.
Ismael Celis pointed out that the article has some similarities to the idea of Parse, Don’t Validate. That article links to another similar one, called Type Safety Back and Forth. That article got me thinking about how the direction from which we attack a problem affects our solution.
Type Safety Back and Forth
Type Design
The article isn’t about outside-in versus inside-out design, so much as it is about the directions which developers do (or should) push “responsibility” around their codebases. Matt says this:
In my experience, developers will tend to push responsibility in the same direction that the code they call does.
That got me thinking. When you build anything with code, you start somewhere. Knowing where you should start can be hard. This problem contributes to the difficulty of teaching programming. Many programmers can’t even tell you how they decide where they start. It turns out that thinking of somewhere you could start and starting there is good enough for a lot of people. (That’s fine.)
I like approaches that, on average, give me good results. One totally reasonable approach that users of languages that aren’t Ruby sometimes choose is to represent their problem (or “domain”) in types. They attempt to model whatever it is they are working on using the type system, then try to build the functionality they need using those types.
This approach has some nice benefits. With a powerful type system, you can model your domain accurately with types. This can help keep the core logic clean, pushing the code that deals with the messiness of the real world to the edges of the system. Matt’s article also shows how a type system can show you that certain things aren’t possible. You can use this feedback to better understand your domain and update your types accordingly.
Matt’s article explores how consciously choosing to push responsibilities backwards (as you might find natural if you started with “inner” domain types) can avoid propagating conditional logic and special cases deeper into your code.
Test-Driven Whatever
TDD heads love outside-in design. We’re all about that shit. From the perspective of where it pushes complexity, this is nearly the opposite of both what Matt was talking about and starting with domain types. Let’s examine this approach through this lens.
It’s touted as a benefit that this approach pushes complexity “down”. I like when I can examine the code at the entry points to a system, i.e. the “high-level” code, and have it be clean an understandable. If I want to understand the details of how the high-level components function, I’ll dive deeper into the object tree, finding more and more implementation details the deeper we get. I like working with designs like this.
Outside-in design encourages these kinds of structures by making it natural to defer handling implementation details until we’re deep into the object tree, having implemented the easier stuff first.
That said, this effect makes the refactor step critically important. If you don’t step back and examine your system, you’ll never notice that there are opportunities to push complexity back up (in your types or behaviour). You can end up with multiple objects at the bottom of your system dealing with complexity that could be handled much higher up the object tree, perhaps in a single location.
Large Plagiarism Models
I’ve enjoyed watching people try to get agentic LLM-based coding tools to work iteratively. Many of the popular tools love spitting out entire, fully-formed classes. When you insist they write tests, they’ll write an entire files full of tests wholesale. Sometimes they’ll even run them. It can be difficult to get them to do anything meaningfully iterative or resembling TDD. (One test at a time, please? Please?)
When you coerce them into an iterative flow with your preferred threat, the results tend to be better than when they are allowed to spit out monolithic changes. The natural flow of iterative development forces them to gain the context they need to make reasonable decisions. Context is king with these tools.
This is no surprise. For us humans, much of writing good, correct code is about feedback. Working from the outside-in (or by trying to model the domain in types) forces us to confront our misconceptions about the real world early, before we’ve made too many decisions on potentially incorrect assumptions. The current generation of agentic tools are basically bad assumption machines.
It’s been cute that many “AI programmers” have discovered iterative development as a technique for getting better results out of agentic tools. Better late than never. The real takeaway is more fundamental, though. The order with which we attack any problem will affect the result. Some approaches are, on average, better than others.
Out of Order
Let’s be clear here. You can build any kind of structure with any kind of technique. Hell, you can write pretty good object-oriented code in C. You’ll find no hard laws of computer science here. Your entrypoint into the problem you’re solving doesn’t decide how your system will be structured.
The order you tackle components merely encourages certain kinds of designs. Us mere humans can’t think about everything at once and your LLM of choice doesn’t have an infinite context window. By deciding what to think about or tackle first, you’re making an implicit decision by omission that will impact the design of your system. As you write more code, you make changing the structure of your system harder and harder.
When I tackle a problem, I start with the area I’m least certain about. If no such area exists, I’ll just start from the inputs and work towards outputs. This, on average, works great.
“A journey of a thousand KLOC begins with a single LOC."—Lao Tzu, paraphrased
Every line of code in a system makes it harder to change. Decisions are most easy to change at the start of a piece work and get harder from there. I can’t tell you where you should start, but I know that you should think about carefully, because those first decisions are the hardest to undo.
Addendum
Justin Searls wrote a thoughtful commentary on this post. I highly recommend reading it. While it sounds like Justin is arguing with me here, I liked this statement:
At the end of the day, every program is just a recipe. Some number of ingredients being mixed together over some number of steps. The particular order in which you write the recipe doesn’t really matter. Instead, what matters is that you think deeply and carefully consider your approach.
Justin found the real takeaway (that I didn’t quite reach in my own conclusion) and made it explicit. Software development (as an individual sport) is a continuous process. You are constantly examining the information you have and deciding how to proceed. The weight of the decisions you’re making varies, as does what (and how much) information you have at hand.
Becoming a more skilled software developer requires examining that process, evaluating changes to your thinking, and adapting.
Upside-Down Development
Consider this one of a thousand signposts I'll erect for the sake of anyone on th...
