Best Simple System for Now
You can have your cake and eat it, as long as you bake it carefully.
‘We can do this the quick way and pay later, or the thorough way and pay now.’ This seems to be a fundamental dichotomy in software development, between ‘perfectionism’ and ‘pragmatism’, but I do not think it has to be a trade-off at all.
A CTO friend uses the metaphor of clearing two paths up a mountain. The left-hand path is quick and dirty, cleared with a machete and brute force–you do not expect anyone to follow you, you just hack your way through–but you make progress quickly. The right-hand path is a wider, clearer, paved path, and more substantial, but takes time to build as you go along. The left path is useful for scouting ahead to see what is coming up along the trail. The right path is where you will lead everyone, knowing that the trail is safe.
My friend lives in a constant tension between folks who take the left path—which also happens to be the fastest way to make money (he works in a trading firm so this argument carries a lot of weight!)—and the right-path folks who want to build a resilient, sustainable product.
This describes many organisations I have worked with. The tensions on either side are based on the idea that the ‘other way’ is doing it wrong. The right-path advocates are worried that the left-pathers are racking up technical debt that is going to come back and bite them, under the guise of ‘pragmatism’. The left-path folks think the right-pathers are ‘perfectionists’, over-engineering and gold-plating things (and there are often plenty of business managers lining up to agree with them).
I propose that there is a middle path both sides are missing. I call this path the Best Simple System for Now. It has taken me a long time to formulate this idea, which has been ‘hiding in plain sight’ for many years. There are several source ideas that either imply this or at least hint at it, which I hope to acknowledge here, but I have never seen it stated explicitly. So here it is.
Characteristics of the Best Simple System for Now ¶
The Best Simple System for Now is the simplest system that meets the needs of the product right now, written to an appropriate standard. It has no extraneous or over-engineered code, and any code it does have is exactly as robust and reliable as it needs to be, neither more nor less.
Each part of the phrase Best Simple System for Now is deliberate and each part is mutually reinforcing. Any deviation in any of these means it is no longer the Best Simple System for Now, but something strictly weaker. Let’s unpack each of these parts.
for Now ¶
When we programmers get our hands on a problem, we bias towards a general solution. There is even an xkcd about it, so it must be true:
People are fundamentally lazy. This is not a slur but a vital evolutionary survival trait. We rely on our instinctive and automatic unconscious to make room for active conscious processing. In Thinking Fast and Slow, Daniel Kahneman refers to these as Systems 1 and 2 respectively. Pattern matching is an unconscious System 1 activity; we are good at it and it comes naturally. ‘Seeing what is really there’ is harder and requires conscious thought and effort.
We are never more than a small leap away from ‘It’s a rules engine!’ or ‘It’s a state machine!’, so our efficient engineering mind decides to save some time–and rework–and reach for this solution right away. Part of this decision is the knowledge that each change carries cost, so taking several small steps to get somewhere is always going to be less efficient than taking the one big step we ‘know’ that we need. While this sounds sensible, there are sound commercial and operational arguments for holding back, which I will get to later.
Designing for now is the antithesis of this. It is the art of seeing what is really there in spite of the patterns that your brain is presenting to you. This is the crux of the BSSN—which might be pronounced ‘bison’, as a companion to all those yaks we usually end up shaving.
Simple ¶
The system should not anticipate the future in any way. This is counter to pretty much any advice I have ever received—or given—as a programmer!
When I am designing something, there are two versions of me, usually in conflict. The ‘clever’ version of me–the one with the decades of experience and the big ego–knows that this thing over here will change next, so we should make that thing an interface. Or that we will almost certainly have this volume of data so we should go ahead and build this concurrent version of the processing algorithm instead of the straightforward serial one. The more humble me knows that I will be close-but-wrong with any of my predictions, in the way I have been close-but-wrong so many times before, so maybe hold off for now. The more humble version usually loses.
So when the future happens, I end up in one of two modes: either working around the assumptions I made because my prediction was not quite what happened next, or backtracking and changing the code back to something simple enough that I can flex it in the way I need. But do not worry, my clever self assures me, this was a one-off, and next time I will get it exactly right! This happens to me time and again. I am well over 30 years into writing software for money so you would think I would have figured this out by now, but no.
Having seen and worked in many codebases over those years, I am quietly confident it happens to everyone else as well. Most code I encounter has speculative seams and interfaces that are decades old and which were never exploited; choices of algorithm and technologies that would have been fantastic for 20,000 concurrent users rather than the twelve people that it serves; hooks and extension points for ‘scalability’ and ‘flexibility’ beyond my wildest dreams, perhaps written in the hope that this would be their magnum opus system, the one that defined their career. And of course this code is littered with subtle bugs due to the interactions between all this complexity.
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
– Antoine de Saint-Exupéry
By this definition, we seek perfection! If you can take anything away from the current system and it still does what it needs to for now, then it never belonged there.
A BSSN does not achieve ‘future-proofing’, ‘scalability’, or ‘flexibility’ by catering for all possible futures, but by being so simple that it can flex in any direction we want. If it needs to handle higher load, we are confident we can achieve that. If it needs to manage a wider range of inputs, or provide different validation, or integrate with unexpected upstream or downstream services, we are confident we can do that too. If it needs to evolve along a dimension we have not even considered yet, we will be able to do that at least as easily as with any other solution.
There are no speculative interfaces, no overly broad data types, no generic functionality where specific code will do. Instead it is highly opinionated and deliberately narrow in its design.
Conversely, I have experienced Gall’s law in the wild countless times, from the enterprise data dictionary to the generic workflow solution to the click-ops configurable rules engine, there is an inexorable 3-5 year arc towards failure.
A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.
– John Gall
Best ¶
Keeping things simple does not mean cutting corners. In any situation there is a right way to do things, and we can choose to do them this way. The challenge for both the perfectionist and the pragmatist is that the ‘right way’ is contextual; code that will underpin core business functionality should have a higher quality bar than a sketch of an experimental feature. Underinvesting in one is as much a risk as overinvesting in the other. In the former case, the risk is stability and resilience. In the latter, opportunity cost, which I discuss below.
Adding to this, the ability to ‘sketch’ code is as much a skill as engineering a robust, resilient solution. Sketching does not mean hacking! I know many more engineers who are good at ‘proper’ code than those who can sketch well. Sketching is about providing just enough quality that you can sustain a direction, knowing you can go back and fill in the blanks later on (or throw away the sketch with minimal investment lost). Hacked code quickly becomes unmanageable. Well-sketched code is just a lighter-weight version of the real thing.
When Ward Cunningham talked about ’the simplest thing that could possibly work’, this is what he meant. He was not invoking some Platonic ideal of simplicity so much as whatever tiny step he could try that would move his understanding forward. In a lovely interview with Bill Venners from 2004, he observes that ’the mere act of writing it organized our thoughts’.
The best code is intention-revealing; it strikes a balance between cohesion and coupling, making it easy to navigate, easy to reason about, easy to change.
If there is duplication, or worse, nearly duplication, of ideas in the code then this is not the best code. Conversely, if the code is obsessively DRY, causing coupling just because the code in two places looked similar, then this is not the best code. If the code does not have domain terms front and centre, mapping directly to your real-world use case, then this is not the best code.
I have written about what I consider good code, joyful code, which I formulated as CUPID:
- Composable: plays nice with other code; has minimal dependencies
- Unix philosophy: does one and only one thing; obviously and comprehensively
- Predictable: in its behaviour, observability, runtime characteristics, failure modes; also in what happens when you change it
- Idiomatic: uses patterns and idioms familiar to a developer experienced in this technology
- Domain-based: intention-revealing in naming, behaviour, and code structure
It is important to note that you can write joyful code just as quickly as–in fact faster than–hacky, ‘pragmatic’ code. CUPID builds on Richard Gabriel’s idea of habitability of code, which he describes as feeling comfortable and confident working with a codebase.
You do not need days or weeks of deep, considered thought to write the best code, but you do need great habits. You develop these habits over time, ideally by working with others who hold these values and who care enough to share their habits with you. I have been lucky to work alongside folks who have been generous with their time and knowledge. Sometimes I talk about them.
The case against BSSN ¶
Let’s look at some arguments against writing the best simple system for now.
It is overkill for a prototype ¶
What is the point of investing in this code, making it ’the best’ code, if we are just going to throw it away once it has served its purpose?
Many prototypes find their way into production, at which point we want to adapt them to do new things. Since the prototype was only ever a hack, there is no good way to add the new functionality, so we wedge it in wherever it will go. Successful prototypes do not get rewritten ahead of release, they get released and then built upon.
When there are only a couple of else-if branches, adding one more will not make much difference. By the time there are 20 or 30 branches, adding one more will not make much difference. There is never a right time when cleaning up this whole mess would be more expedient than adding just one more edge case or condition, unless we choose it.
So our options are to live with the mess, rewrite it, or never get into this situation in the first place. One of my favourite technologists, Randy Shoup, says that if a digital business does not rewrite its entire stack as it scales up, it probably over-engineered in the first place. I believe this is true in most cases, but in the handful where they stuck with the best simple system for now, they simply never needed to.
Before its acquisition by Meta, WhatsApp scaled to over 500 million active users with an Erlang codebase built and managed by about 13 engineers. Similarly, the SQLite relational database is among the most widely deployed pieces of software in history, installed in practically every browser, desktop, laptop, mobile device, server, and a host of other contexts. Its core team comprises three people and they do not take code submissions.
It is incomplete ¶
If you know what the product needs to do, why should your users tolerate a half-baked version?
In short, because meeting part of a customer’s need sooner is a win-win. Most customer needs take the form of a power curve: the main benefit occurs in part of the functionality and the remainder is a diminishing return of marginal gains. Getting money out of an ATM is better than also getting a printed receipt, seeing your balance, changing your PIN, and so on.
The first iPhone, released in 2007, was a 2G device in a 3G world. It would not reach network parity with other smartphones for another year, and it would be a year after that before you could copy and paste between messages and email! Still, the iPhone stole the market from underneath ‘unshakeable’ incumbents like Nokia and BlackBerry, while quietly adding these features to later versions.
Some products differentiate themselves by having a small-but-opinionated subset of functionality. Google Docs only does a fraction of what Microsoft Word can do, by design. Likewise Google Sheets with Excel. Google Workspace has become a contender to Microsoft Office because this small subset covers most people’s use cases most of the time, and comes with the benefit of not needing any software installed, updated, patched, secured, or audited, other than a browser. This makes procurement and lifecycle management attractive for enterprises, while the learning curve is gentle for new users.
The argument for small, frequent delivery, early customer success and rich feedback has been made many times. BSSN adds the observation that shipping a partial product should not mean compromising code quality, speed of delivery, or user experience.
Richard Gabriel–he of the habitable code–describes this idea as ‘worse is better’. In the early 1990s, he was disappointed but not surprised that the C language was becoming ubiquitous over Lisp, which in his mind was superior. Why would an incomplete, inconsistent, overly complicated language like C be preferable to a simple, correct, consistent and complete language like Lisp? He goes into an entertaining diatribe about how C and Unix are the perfect computer viruses, and why ‘worse is better’: shipping an incomplete product and iterating on it is better than waiting until everything is ready, even if the iterated version never becomes ‘complete’.
In an aside, comparing Common Lisp and Scheme, he describes a different dichotomy, between the big complex system which takes a lot of effort to design and build and which requires complex tools to operate, and a diamond-like jewel that ’takes forever to design, but is quite small at every step along the way’. This latter sounds a lot like a Best Simple System for Now, the difference being that with the BSSN there is a functioning, usable system at every step.
It is inefficient ¶
Why constantly refactor and rework the code? Why not get it right the first time?
When you choose to spend time and money developing a product, you are making a decision based on investment risk. The money you expect to make needs to exceed the money you expect to spend, with enough margin for you to be comfortable starting. This is known as Return on Investment (ROI), or Return on Capital (ROC). The less we can spend on building the product, the more profit we will make, assuming the same revenue. So why would we want to continually rework a product? Surely this will erode our profit margin.
The answer is to look at when you make that money. In finance terms, money today is worth more than money tomorrow, or rather, money tomorrow carries more risk than money today. The further out the payoff, the greater the uncertainty. The money you spend building something that you hope will have a return in the future is called Value at Risk, or VAR.
Big-bang projects with a Grand Release at the end are loading up VAR throughout the life of the project. If the release gets cancelled, or if the product tanks after release, then all that investment is gone. Releasing versions of your product early and often allows the revenue to start flowing sooner, or lets you learn that the product is a flop sooner. Either of these is a win. In the former case, your programme becomes self-funding early on and is a lower investment risk. If the product is a flop, you did not spend much learning this. Not only is the customer delighted that we shipped a partial product as soon as we could, all that revenue has been going towards the next tranche of development.
Maintaining the Best Simple System for Now means you can act on that feedback whatever it may be. You can extend the system in whatever direction you need, knowing that you are starting from a consistent, reliable baseline, and committing to keep the simplest and best solution as you go along, for now.
Taking VAR into account gives you Risk-Adjusted Return on Capital, or RAROC. Essentially you scale the investment by the value at risk, which exposes a riskier timeline as a likely worse investment. I was introduced to this concept by my former colleague, trading and risk guru Chris Matts.
In short, iterative delivery tends to offer worse ROI–once you factor in all those releases and continual planning activities, as well as all that churning code–but better RAROC, which it turns out is what matters.
In his seminal book The Principles of Product Development Flow, Donald Reinertsen offers an additional perspective on this. Imagine you could snap your fingers and the customer’s need was met and their problem was solved. You would be making them happy straight away and they would be paying you straight away. Now think about every hour, every day, every week, every month that it takes to get the solution in front of the customer, before you can meet that need and the customer chooses to buy your product. This is all dead money that you will never have. This is called the Cost of Delay.
Alongside this is Opportunity Cost, which is the value of the most valuable thing you could have been doing instead. If your team is spending months on something, they are simutaneously not doing everything else!
Reinertsen explores the economics of product development and shows how ‘value costs’ like cost of delay or opportunity cost dwarf ’effort costs’ like the project burn rate, wages, or licences. We obsess about the latter while the bigger prize is hiding in plain sight. All of our project metrics are around activity and effort–utilisation, productivity, time sheets–instead of lead time or throughput. Reinertsen advises us to ‘measure the work items, not the workers.’
Rather than trying to reduce activity or effort, I focus on reducing the delays and hand-offs, and slicing the work into smaller releases. I call this looking where the action isn’t.
Why do we not do this? ¶
As thinkers from Voltaire to Stephen Covey have observed, common sense is not common practice. People tend to follow the herd; we have limiting beliefs or received wisdoms, starting with the supposed dichotomy between pragmatism and perfectionism, and reinforced by arguments such as those above.
Computing pioneer Grace Hopper described this attitude of ‘We’ve always done it that way’ as the most dangerous phrase. We should challenge this wherever we find it.
In his Seven Habits of Highly Effective People, Stephen Covey defines a scarcity mindset as the belief that life is a zero-sum game where everything is a trade-off. This is our default state. in contrast, he suggests adopting an abundance mindset which looks for the win-win in every situation. One is fighting for ‘your share of the pie’; the other is figuring out how to make a bigger pie.
From the scarcity perspective, there is indeed a conflict between doing the job right and getting something out the door. It takes effort and discipline to train yourself into an abundance mindset, whereby both things can not only be true, but mutually reinforcing.
Kent Beck exhorts us to ‘Make the change easy, then make the easy change.’ I believe we need discipline, habits, courage, and humility to keep code simple, so that any change is easy. We do not have to predict the future if we can adapt freely to it.
Discipline ¶
Keeping the kitchen clean as you go along is an unending series of deliberate choices. Washing up as you go, putting things away, keeping knives sharp, keeping the shelves stocked and organised, are all extra work on top of the core function of cooking.
A kitchen whose team that has this discipline–and it only works as a whole team discipline–is an efficient, buzzing hive of activity, where people know where to find things and can trust the tools they are reaching for.
Whether or not you do this creates a reinforcing loop. A tidy kitchen enables sustainable delivery and keeps cooking fun. It makes you want to keep it that way. A messy kitchen is a continual frustration of dirty implements and missing or misplaced ingredients. No one bothers to tidy up when the kitchen is already messy. This is the broken windows effect.
Habits ¶
There is no arcane or deep knowledge required to maintain a Best Simple System for Now. It takes daily resolve and good habits. It takes effort and deliberate practice to train yourself to ‘unsee’ the general case; to not reach for a library first ‘because it is only adding a single dependency’; to not prematurely abstract or DRY code because reasons; to not over-invest in a component because we have blanket coding standards with arbitrary rules like percentage of test coverage; to go back and stabilise that component you sketched to see if it would work, and it did; to excise that component you sketched that did not work out.
The way I learned to do this was partly pairing with people who already have this aesthetic, partly dogged persistance. I have learned to recognise when I am over-working an idea, or under-investing in something that needs to be robust. I am still not great at it and I often will not notice until I am some way down a rabbit hole, but I have at least developed the muscle enough that I can work with someone else and show them this middle path.
Courage ¶
How could this possibly work? And if it is that good an idea, why is everyone else not doing it? Taking a leap into the unknown is tricky. Courage is not lacking fear, but deciding to press on anyway. I encourage the principle of ’trust me once’. If we try this thing and it does not work, we can always back it out and do the other thing. It is only code, and we have version control, right? But if it does work… well, how cool would that be?
Humility ¶
Bruce Lee used to tell his students to ‘be like water’. If you pour water into a cup, it takes the shape of the cup. If it flows into a river, it takes the shape of the river. Flexibility comes not from anticipation, but from simplicity. Nothing is simpler than flowing water!
When we change the system in response to new demands, we evolve it towards a new Best Simple System for Now, in the context of a new ’now’. Remember those two versions of me? The habit to develop is learning to trust the one who knows they do not know rather than being railroaded by the one who knows what the next steps should be.
Where do you start? ¶
The best way to adopt BSSN is to Just Start™. As with any new skill, you will get it wrong at first, overindexing one way or the other, while you get the hang of it. So be kind to yourself and cut yourself some slack. Stepping out of your safety and comfort zone is where the learning happens, but it might be a bit bumpy.
The thing I found hard–and still do–was seeing the ‘for now’. It is so tempting to define that interface, extract that ‘reusable’ component (just in case, because I am clever!); or conversely to under-engineer because I underestimate how quickly those micro-investments of quality and effort will pay off (and I don’t need those tests or that consistent domain language anyway, because I am clever!).
The good news is that the more you practise these habits, the easier it becomes to build the Best Simple System for Now. I can reach for TDD almost as fast as I can ‘just code’, and always with better outcomes. Conversely, I might code-run-observe on a command line (known as a REPL or Read-Eval-Print loop) while I am sketching something. As long as I am getting feedback, it does not matter yet whether that feedback is repeatable or automated.
As a REPL example, I was writing some custom fixtures for PyTest recently–testing a test framework is hard!–and some of these involved creating and manipulating temporary files and directories. While I was sketching and exploring how I wanted the fixtures to work, it was much easier to run a few tree
and grep
commands than it would have been to write tests to check where the files were being written. Plus, these kinds of test end up as ‘double-entry bookkeeping’, reproducing the implementation of the thing they are testing, which is brittle and not very helpful. If PyTest changes how it does temporary files, I should not need to care as long as I still get the behaviour I want.
What are you working on right now? How could you reframe some subset of this as a Best Simple System for Now? Start there and see what happens. If this is difficult, take that as an indicator of where you will need to put in some effort to ‘make the change easy’. Do not try to do this all at once. Choose something that will have an early payoff so you can build credibilty with your teammates, and with yourself, about how this works and how it is different.
Here are a couple of examples.
Example: the JSON library ¶
One trading client wanted my advice about which JSON library they should choose for a business-critical trading application written in Java. Two of their senior engineers were at loggerheads backing different solutions and showing no signs of compromise. Various characteristics were being thrown around: scalability, resilience, transitive dependencies, community support, and so on. They wanted the external consultant to act as the tie-breaker.
Rather than pick a side, I asked how many different entity types they were planning to send over the wire. After some discussion, it emerged that there were nine. Nine types. I suggested that maybe they did not need ‘a JSON library’, with all its runtime quirks, transitive dependencies, and complexity. Maybe they just needed to marshal and unmarshal nine types of object.
As an experiment, we defined an interface that each of the nine types T
would implement:
interface Jsonable<T> {
String toJson(T obj);
}
and we required them to have a static method like this (other languages might make this pairing more symmetric):
class SomeT {
public static SomeT fromJson(String json) {
// ...
}
}
They implemented these, with tests, in nine places. Nine pairs of one-line methods, zero dependencies, lightning fast perform ance, minimal attack surface; the best simple system for now. If they added a tenth type, they would know exactly how to extend the solution and exactly how it would behave. If they wanted to change the format for any type at any time, they could do this trivially. And so on.
Example: the XML streamer ¶
Let’s have another Java example, this time from the early 2000s. In an eerily similar situation, we needed to stream some Java objects around the place. This time we did not know in advance what they would be, but we did know that the existing solutions were overkill for what we needed.
This was in the heyday of Enterprise Java, when the world ran on XML, and you would use XML to XML your XML, or your other XML.
All we wanted was to render an object like this:
class Boat {
String name;
int length;
boolean isSubmersible;
}
new Boat("Boaty McBoatface", 5, true);
as this:
<Boat>
<name>Boaty McBoatface</name>
<length>5</length>
<isSubmersible>true</isSubmersible>
</Boat>
So my buddy wrote a little library to do just that, and nothing more. Its entire USP was that it had no bells and whistles. Over time it could handle more and different edge cases, such as self-reference and other pointers, but it only ever had one boring and predictable way to render the data as XML, or to parse and load from XML.
As an early adopter of open source software, he submitted this library, called XStream, to an OSS hosting site, an ancient forerunner of GitHub. They rejected it on the grounds that was ’too simple’ so it was not worth hosting. Over 20 years later, XStream is still going strong and is practically everywhere that Java is, including on the International Space Station! It is also still simple and obvious.
These examples are not to suggest that a BSSN is always home-grown, or to encourage a culture of not-invented-here. With XStream, my buddy was happy to build a community around it so he did not have to think about it any more!
Instead they are about ‘seeing what is really there’ and solving for that use case alone. If a third-party library or tool is the best solution for now, then you should use that. But look holistically at the cost of adoption, learning, quirks, transitive dependencies, working around its limitations when they do not align with your goals, and the cost and effort of ripping it out later, rather than just the short-term convenience of not having to write a few lines of code. And if you do decide to write something yourself, write only what you need for now.
tl; dr ¶
You do not have to choose between gold-plating dressed as craftsmanship or perfectionism and corner-cutting framed as pragmatism or realism. You can have the quality of the former at the speed and focus of the latter. I call this the Best Simple System for Now.
You can choose to conscientiously evolve a codebase so that at any point in time, it is ‘as simple as possible but no simpler’, while having joyful, CUPID characteristics, comprising small components that talk to each other like cells passing messages, as Alan Kay once described object-oriented programming. Not that OOP is the only way, or even the best way, to achieve this. It is just one example of what I call a replaceable component architecture.
The skill to develop is to see what is really there and to solve for that, well, rather than being seduced into solving the general case or believing that hacking is the only way to move quickly.
Write the software appropriate to the context–whether that be a ‘sketch’ or robust and resilient code–confident in the knowledge that you can evolve, replace or delete this once you learn more.