Designing and building useful software products that serve well their intended purpose, both for customers and the business, is often an intimidating challenge.
Traditionally, falling into the big upfront design —also known as “planned design”— was the standard way to go. We could see its primary expression in the waterfall approaches from the 70s that were still a common practice among development teams not so long ago.
The upfront design approach, however, lies on the assumption that the knowledge and understanding we hold about the domain we are modeling is perfect. And as we’ll see below, this assumption turns to be utterly wrong.
A novel approach came later upon the arrival of the Agile philosophy and other related methodologies such as XP or Scrum. That approach was evolutionary design, and, in theory, it was supposed to fit reality better.
However, people progressively deteriorated the meaning behind the principles in Agile by applying them in the wrong way.
People usually fell into the code and fix approach, which turned out in doing no design at all.
But completely forgetting about design, typically does more harm than good. It often causes software systems to become a mess that is hard to evolve (unless we talk of straightforward systems). We could call it evolutionary design done wrong.
What experience tells us about both approaches is that they often result in software that doesn’t ship or failed products that offer value to nobody. Not to mention the waste of resources they lead to, and the frustration and apathy they breed inside teams.
The premise: software systems operate in a dynamic world of unknowns.
When we start a brand-new project, our comprehension of the status quo is never entirely right. And, logically, neither are the solutions we come up with to improve it.
But even if we manage to get the problem and solution right, we should take another no less important point into account: the world is not static.
Problems we shape somehow today, are re-shaped some other way tomorrow. People circumstances shift, laws & regulations change, different alternatives come into play, other adjacent problems arise, and new constraints appear.
In the end, this brings us to a foundational point: the world, the market, and our understanding about them are in continuous change.
Therefore, software systems need to evolve to survive, either if we want it or not —as any natural system.
We should design software solutions with this notion of evolution in mind. And there is a sure thing, both from the planned and code and fix design approaches. None of them facilitate learning, nor do they help us design software that is easier to evolve.
Evolutionary design the right way.
Going back to the original principles of evolutionary design is the key to sustain learning and build software systems that can evolve more quickly and more efficiently. Systems that can better adapt to reality and ultimately translate into useful solutions that solve real problems.
Evolutionary design is about progressively improving the design as we learn more about the domain, instead of forcibly making unnecessary upfront choices based on risky assumptions.
What this involves, among other things, is the need for lowering the cost of change, as the means to allow and empower that evolution.
And how all this translates into practice?
In a more practical sense, evolutionary design works upon the following foundational principles and practices:
- We must design iteratively as part of the programming processes.
As the program and our understanding of the domain evolves, our design should change as well.
- Along the process, we must place our focus on those things that are riskier, the things we know will be certainly needed and hard to change later.
Said another way, we shouldn’t place too much energy on those design decisions that might be easily reverted later.
In these situations, we should lean towards the most straightforward design that fulfills what’s needed and avoid over-complicating it with dubious stuff that probably won’t be required. If you cannot make it simple, then perhaps you should go back and narrow down your definition of the problem further.
- We must build software in a way that makes it easy to change.
That involves the introduction of some standard Agile development practices aimed at enabling precisely that. And it’s also why evolutionary design cannot work successfully without their support.
“Simple design,” refactoring, use of design patterns in an evolutionary style, automated testing, continuous integration, and continuous delivery are some of those practices. But also any other thing that can flatten the change curve.
- We must improve human collaboration and feedback.
We can do that by leveraging other usual practices such as daily stand-ups, team retrospectives, or getting user and stakeholder feedback, to name a few.
I will not get further into this, as there are many resources out there covering these topics. If you want to dig deeper, you can take a look at this excellent article by Martin Fowler.
But…is actually software the most flexible option to support learning (and uncover the unknowns)?
As seen above, the success of a project hugely depends on the understanding we have about the problem we aim to solve and how strong that knowledge is.
As tech makers, we often do not pay attention to the importance of this factor and assume that the requirements we get are right.
But there might be many wrong assumptions, and, usually, we should not start a software project until uncovering those first. The reason: software might be extremely costly, hard, or even impossible to change if the foundations are not stable enough.
We can find many different example scenarios in which starting with software could become a downright mistake.
- First, we can get the solution slightly wrong. That is not the worst case. We understood the problem well, but the solution still has some spots that did not work out as expected. We have the right focus. We could always keep iterating and evolving our software design. Yet, this might not be precisely the most agile and cheapest alternative.
- Something worse is getting the problem wrong, or not narrowing it down enough. That is a dangerous situation. When this happens, we could end up with a wholly misaligned solution that we’d have to throw away and build again from the ground up —after an enormous investment, if not too late already. We could also come up with an oversized solution to cover many potential use cases. However, in practice, they usually will never happen, and they will expand the scope in such a way that we won’t be able to handle it.
- Even worse, we could fall into a more dramatic situation when we focus on the wrong problem from the beginning. That usually means going back to the starting line —not a very desirable position to be in.
Those are just some examples of things that might doom our software projects. These are all elements that would be quite hard to fix once we have started building a solution. Or even worse, once we’ve gotten it out into the market already.
Hence, we shouldn’t blindly follow requests or what we think should be right. We should uncover critical unknowns first. Otherwise, we lose an excellent opportunity to influence our team and customers positively.
By falling into that trap, we lose a great occasion to improve the chances of success of our projects.
Once again, this drives us to another foundational question: how could we better our comprehension of the problem and uncover those unknowns first?
Well, this is a massive topic in itself. Still, a fundamental idea we should take into account is that software might not be the best instrument to use.
Ideally, we should not get into software development until we’ve gained a comfortable level of confidence about what to build.
That is not entirely achievable, I know. There will always be remaining uncertainty to uncover. However, this is not about reaching a hundred percent certainty, but removing the most significant risks and gaining enough confidence to avoid potential rabbit holes before starting.
And towards that end, we might find better and more efficient answers on top of design research, experimentation, prototyping, and prioritization. These are all crucial pillars that, from my experience, should be leveraged throughout the whole lifecycle of a product.
Those are invaluable skills to have in our projects.
They can help us discover insights that will allow us to get in the right direction before raising our bet and draining our energy and resources.
We might discover that a problem does not even exist. Or that it does not worth the investment. Or that a software system is not the best solution for it.
We might find out what would help us deliver real progress for people and the business —all before writing a single line of code.
In essence, it all comes down to learning and managing risk at the lowest possible cost. And coincidentally, this is the essential idea behind philosophies like Lean Software Development or Lean Startup.
It’s in our hands to grow, raise our perspective, learn about those critical skills, and improve our toolset.
They are potent abilities and, without the slightest doubt, will help us collaborate better and enhance our level of influence. And no less important, they will enable us to make better engineering decisions, and increase the chances of success of our projects.
DDD as a practical path to evolutionary (software) design.
Up until this point, we’ve talked about evolutionary design as a general philosophy. We’ve also spoken about it as a set of specific foundational practices around software design and development that can help us navigate this river.
However, there is still another vital spot.
Assume we did our homework. We lowered the risk and uncertainty. We feel confident about what to build. Now…how can we proceed with the design of that system, from an engineering standpoint?
Once it’s time to start building, I tend to leverage an approach that embraces, encourages, and empowers agile and evolutionary design in the context of software: Domain-Driven Design (DDD).
DDD is a framework that provides a set of principles, strategies, and tactics aimed at tackling and accelerating software projects that have to deal with complicated domains.
Fundamentally, DDD relies upon centralizing business and technical knowledge among team members, creating a unified shared perspective of the domain we work on.
It’s a great tool, but successfully applying it depends hugely on the point outlined before: the foundational knowledge we have about the domain must be trustworthy.
Again, ideally, we should be pretty sure about the critical unknowns that influence parts of the system, which might be hard or even impossible to alter later. We should know we are tackling a problem that is worth solving. We should understand and narrow it well. And we preferably should test and validate our design to some extent first.
If this happens, things will turn more natural, and we’ll be on a much better road towards shipping the right thing.
- Traditional design approaches, such as planned design or code-and-fix, tend to result in adverse outcomes. Projects we can’t ship, failed software products that offer value to nobody, wasted resources, and frustration and apathy among teams are some of the habitual results.
- The world, the market, and our understanding of them are in continuous change. Hence, we should design with this notion of evolution in mind.
- Evolutionary design is a philosophy that honors that vision and focuses on evolving our design as we improve our knowledge about the domain in which it is to operate.
- What underlies beneath this concept, it’s not so much a matter of flexible software but progressive learning and validation. Flexibility is an enabler for that.
- It follows that the principles of evolutionary design apply beyond software systems. That is what the Lean Startup movement in the world of entrepreneurship is all about.
- In reality, building software might not be the most flexible and cheapest path for learning and understanding, especially in the early stages of a project.
- Some of that learning might be better acquired through other more agile and cheaper means, leveraged on pillars such as design research, experimentation, prototyping, and prioritization. Leveraging those, might lead us to surprising discoveries and determine the fate of our projects before even writing a single line of code.
- Domain-Driven Design (DDD) is a great tool to help us evolutionarily design software. That is especially true once we have a narrow problem definition and feel confident enough about what to build.
In essence, by embracing evolutionary design principles and practices, we’ll be in a much better position to improve our odds of shipping great software that helps people and the business. All while making efficient use of resources, increasing our margin to course-correct, and boosting the excitement and motivation among our team.