When I started writing software, we had Architects (plural, with a capital A). The Architect would work with the business and with other Architects to compose a High Level Design Document (HLD), which was often surprisingly lengthy and quite low-level in content. Engineers would then be tasked with implementing the design documented in the HLD. This practice has come to be known as Big Design Up Front, and has fallen out of favor in recent years.
I was never a fan of Big Design Up Front. It made projects take far too long and led to a lot of sunk-cost justifications.
Implementers always discover questions that only the designer can answer, but which are not visible to anyone but the implementer. The HLD is out of date before it comes out of the printer.
The tragedy of Big Design Up Front will be a topic for another day, but in it’s place arose a different problem: No Design Up Front.
In this methodology engineers write code extemporaneously, without any design at all. When that code doesn’t do what the business wants, the engineers write more code, and faster! After a few go-rounds they convince the business that the code is a perfect solution and the business goes off to find a better problem.
The code that results from No Design Up Front is usually about what one should expect from the process: tangled and unexpected dependencies, mysterious names, poor separation of concerns. I find this code is heavy on technical do-dads but not very obvious about the business problem it purports to solve. I’ve also found that teams who practice this way are not able to explain their own designs to each other or a newcomer.
Honestly, I’d rather inherit the product of a Big Design Up Front project, which will at least have hundreds of pages of text and diagrams to help me understand what it was intended to do.
A few things I’ve learned about software design:
- Software design is best done by the same individuals who build the implementation
- Software design is more about the problem to be solved than about the technical means of implementing a solution
- We’ll make a mess of our design if we do too much designing without implementing
- Good design does not happen by accident, we need to put some thought into it
Design uninformed by implementation risks becoming irrelevant and bloated. Implementation uninformed by design risks becoming repetitive, hard to understand and less conducive to reuse. Undesirable dependencies and interactions can creep in because we need to take a higher-level view to spot them.
This has lead me to a practice I’ll call Little Design Up Front.
Before you start designing, understand the problem
This should be a given, but it’s not. Too many teams charge into implementation without understanding the problem they are tasked with solving.
Personally, I believe this is the single best indicator of a project’s future success and adoption. With my projects, I have a goal that every team member should be able to explain the goals of the project to an uninformed outsider before we start writing code.
This doesn’t take very long to achieve, a few days of concentrated effort will suffice.
Before you start writing code, spend a little time designing a solution
Design an end to end solution to your problem, but at a very high level. Keep it grounded in the business problem at hand.
As for technical design, focus on the abstractions and their relationships. Give a little thought to how you might implement your design but don’t commit to anything yet.
Draw your initial design on a whiteboard and talk through it with your team
This will have a few effects:
- Ensure consensus among the team members
- Spot unwanted dependencies that are hard to see when you’re deep in code
- Identify relevant design patterns
I’ve found that a lot of engineers don’t want to bother with this step. “It’s in my head, I know what I’m doing!” Unfortunately, every head on the team contains a slightly (or significantly) different design, solving slightly different problems according to each person’s understanding. Remember the story of the blind men and the elephant. Getting your design onto a whiteboard where others can see it, comment on it and contribute to it is critical to achieving consensus.
Use UML-ish representations but don’t try to make it perfect. Annotate the drawing freely with whatever explanatory text or symbols you deem useful. Take a picture of it and erase the whiteboard. Draw it again, then again. Draw it one more time.
Write some code, based on your design, but depart from your design as needs arise
When you begin your implementation, use your simple design to decide a reasonable starting point, but don’t feel constrained to doing exactly or only what is specified in the design.
You may find that some of the classes or dependencies described in your design don’t seem useful or appropriate. You may discover other classes, design patterns and dependencies that make sense for your solution but which are not specified in your design. You may find useful code in your project, which, if restructured carefully, could be useful in your solution.
Periodically draw the design that is emerging in your code
Periodically while implementing, take a few moments to quickly sketch and annotate the design that is emerging in your code. If your implementation work has required you to figure out the architecture of some other bit of your project, go ahead and sketch that as well. Talk through the emerging design with your peers.
Just as before, this diagram and conversation will make your program structure, patterns and dependencies obvious.
Look at your new diagram and consider a few questions:
- Do the dependencies and patterns you have implemented so far look reasonable?
- Do you like the class and package names you have chosen?
- Are there technical details that are forcing you to accept less than desired structure or dependencies? If so, annotate your diagram to explain that situation. Is there a design pattern emerging? Are there opportunities for reuse that you didn’t notice while implementing?
Update your new drawing with whatever you decide should change, then change your code to match. Continue implementing, and repeat this exercise later.
What do we gain from this?
By iterating over this design, implement, design, implement routine, we can arrive at a design that is informed by implementation details, and an implementation that exhibits traits of good design like sensible dependencies, obvious intent, names and structure reflective of both the technical constraints and the business problem modelled by the code.
We will be fluent in our design, able to explain it to anyone.
Another team inheriting our code will be able to see both the problem we attempted to solve and the design we arrived at without too much investment reverse-engineering it.
That future team will probably think well of us, at least better than they feel about the authors of the No Design Up Front mess they inherited last year!