This week I finished reading A Philosophy of Software Design book by John Ousterhout. I found the book easy to read and moderately thought provoking.
The book talks about two approaches to reduce complexity of software systems – 1) embracing simplicity 2) embracing modular design.
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.
We know we are working with a complex software system when:
- To implement a small feature or fix a bug we have to make code modifications at many different places.
- Developers need to know more than they should to make a simple change.
- We don’t know what else we need to change to implement a simple change
To handle complexity we have to apply a strategic mindset to programming. This is difficult to achieve in typical Agile software development that we do these days. We are all focussed too much on delivering the feature or fixing the high priority defect. This is what the author calls a tactical mindset. To apply strategic thinking you have to go slow to move fast. In my experience this depends a lot on software architect. A good software architect can keep the big picture in mind and keep design coherent. They keep the conceptual integrity and long term structure of the system in mind and take decisions accordingly.
Few ways you can apply strategic mindset in Agile environments are:
- Allocating time in each sprint to work on technical debt. We kept 1 day per team member every sprint to work on technical debt.
- Scheduling tech debt sprint every quarter. You can use this time to update dependencies to the latest version or other technical debt items.
- Following a formal design process for designing bigger changes. You should write design documents to collaborate on the design. I use this design document template.
- Scheduling slack time in the sprint
As your engineering team grows you will be forced to use a strategic mindset else your team’s velocity will decrease and they will slow down.
Facebook changed its motto from “Move fast and break things” to “Move fast with solid infrastructure” to encourage its engineers to invest more in good design.
To handle complexity, author recommends that we use modular design.
A module is any unit of code that has an interface and an implementation.
A module could be either a class, a library, or a service( or these days we call them Microservice).
We should design modules that are deep in nature. This means they should have simpler interfaces but can have complex implementations. A module interface should provide high level abstraction to the module client so that they don’t have to depend or care about implementation details.
The point about simple interfaces was also mentioned in the post on AWS S3 design that I read last week. AWS S3 only had four(PUT, GET, DELETE, LIST) operations when it was launched.
If you build a complex system, it’s much harder to evolve and change, because you make a lot of long-term decisions in a complex system. It doesn’t hurt that much if you make long-term decisions on very simple interfaces, because you can build on top of them.
A module interface should make it as simple as possible to achieve the common use case. Java’s File I/O breaks this principle. Every file I/O wants buffering so it should be provided by default. If you design an API that expects an options/configuration object then make it possible to call the API without passing any options by using sensible defaults for those configuration values.
Encapsulation and information hiding is the best way to create a simpler interface. This can be achieved by not exposing internal data structure as getters. This is the most common API design sin. This leaks API internals and clients become tightly coupled. Unit testing with mocking also breaks encapsulation. So, I recommend not using mocking.
The chapter 9 of the book discusses an important topic: Given two pieces of functionality should they be implemented in the same place or should their implementation be separated. This is highly relevant in the context of Microservices. Author suggests the following.
- Bring together if information is shared.
- Bring together if it will simplify the interface
- Bring together to reduce duplication
- Separate general-purpose and special-purpose code
You can use them as heuristics to decide whether you should divide a service into smaller services or keep it together.
There are a few more good ideas in the book that we can use in API design. These are:
- Exceptions make an API interface complex and forces clients to handle them. Author suggested two techniques to handle exceptions – masking exception and exception aggregation. In exception masking, you handle exceptions at a lower level so that you don’t have to handle it at a higher level. Exception aggregation talks about handling many exceptions with a single piece of code.
- Design it twice to come up with better design. Using design documents helps here a lot.
- Using comments and documentation as a means to improve system design. Comments should describe things that are not obvious from the code. Keep docs next to the code base so that there is less friction to write documentation.
- Good naming simply code as it makes it easy for others to understand the code. Good names are consistent and precise.
- Don’t use generic containers like Pair, Triplet, etc. I used that in one of my projects where I used JavaTuple library. It was fun when I was writing the code but when I had to debug it was troublesome. Software should be designed for ease of reading, not ease of writing.
I recommend reading this book. It gives practice advice to software design. You can start applying lessons in this book immediately as they seem obvious and logical.