📅 October 6, 2024

Book Review | Modern Software Engineering

I recently read Modern Software Engineering. This book was one of my most highlighted books I have read.

Something that stood out to me was that this book was published in 2021 before the release of ChatGPT and there was a paragraph from the book that seems even more relevant now in 2024:

Professional programming isn’t about translating instructions from a human language into a programming language. Machines can do that. Professional programming is about creating solutions to problems, and code is the tool that we use to capture our solutions.

My favorite highlights from the book are below.


Engineering is about adopting a scientific, rationalist approach to solving practical problems within economic constraints.

Software engineering is the application of an empirical, scientific approach to finding efficient, economic solutions to practical problems in software.

Any approach worth the name of software engineering must be built around our need to learn, to explore, and to experiment with ideas.

Our discipline is fundamentally one of learning and discovery, so we need to become experts at learning to succeed, and science and engineering are how we learn most effectively.

Software development, unlike all physical production processes, is wholly an exercise in discovery, learning, and design.

The art of programming is the art of organizing complexity.

You make a decision based on the evidence before you and your theory of what that will mean, and then you test your ideas to see if they work. It is not some perfectly predictable process.

Throughput is a measure of a team’s efficiency at delivering ideas, in the form of working software. How long does it take to get a change into the hands of users, and how often is that achieved? This is, among other things, an indication of a team’s opportunities to learn. A team may not take those opportunities, but without a good score in throughput, any team’s chance of learning is reduced.

Software development is an exercise in exploration and discovery. We are always trying to learn more about what our customers or users want from the system, how to better solve the problems presented to us, and how to better apply the tools and techniques at our disposal.

It’s best to start off assuming that our ideas are wrong and work to that assumption. So we should be much more wary about the potential explosion of complexity in the systems that we create and work to manage it diligently and with care as we make progress.

The phrase that best captures the ideas, maybe ideals, of the agile community is "inspect and adapt."

Waterfall processes can be effective for some kinds of production problems, but they are an extremely poor fit for problems that involve exploration.

Iteration is at the heart of all exploratory learning and is fundamental to any real knowledge acquisition. If we approach software engineering as an exercise in discovery and learning, iteration must be at its heart.

Perhaps the most important idea is that if we start to change our working practices to work more iteratively, it automatically narrows our focus and encourages us to think in smaller batches and to take modularity and separation of concerns more seriously.

Surprises, misunderstandings, and mistakes are normal in software development because it is an exercise in exploration and discovery, so we need to focus on learning to protect ourselves from the missteps that we will inevitably make along the way.

Industry data says that for the best software companies in the world, two-thirds of their ideas produce zero or negative value. We are terrible at guessing what our users want. Even when we ask our users, they don’t know what they want either. The most effective approach is to iterate. To accept that some, maybe even many, of our ideas will be wrong and work in a way that allows us to try them out as quickly, cheaply, and efficiently as possible.

To make progress we must take a chance, make a guess, be willing to take a risk. We are very bad at guessing, though. So to make progress most efficiently, we must organize ourselves so that our guesses won’t destroy us. We need to work more carefully, more defensively. We need to proceed in small steps and limit the scope, or blast radius, of our guesses and learn from them. We need to work iteratively!

Feedback allows us to establish a source of evidence for our decisions.

Software developers are not paid to make nicely designed, easily testable software. We are paid to create value of some kind for the organizations that employ us.

Individuals and Interactions over Processes and Tools.

One of the defining characteristics of high-performing teams in software development is their ability to make progress and to change their minds, without asking for permission from any person or group outside of their small team.

We need to accept that change, missteps, and the impact of the unexpected, as our knowledge deepens, are all simply inevitable, whether you acknowledge them or not. It is simply the reality of all complex creation of any kind.

Accepting that we don’t know, doubting what we do know, and working to learn fast is a step from dogma toward engineering.

One of the deeply important aspects of a scientific rational approach to solving problems is the idea of skepticism. It doesn’t matter who has an idea, how much we would like the idea to be true, or how much work we have put into an idea; if the idea is bad, it is bad!

Science works! Make a hypothesis. Figure out how to prove or disprove it. Carry out the experiment. Observe the results and see they match your hypothesis. Repeat!

Organizing our thinking, and our work, to proceed as a series of experiments to validate our hypotheses is an important improvement in the quality of our work.

Infrastructure as code allows us to be more precise about the environments in which our software operates.

We can think of a service as code that delivers some "service" to other code and hides the details of how it delivers that "service." This is just the idea of "information hiding" and is extremely important if we want to manage the complexity of our systems as they grow.

Identifying "seams" in the design of our systems where the rest of the system doesn’t need to know and shouldn’t care about, the detail of what is happening on the other side of those “seams” is a very good idea. This is really the essence of design.

A system is not modular if the internal workings of adjacent modules are exposed. Communication between modules (and services) should be a little more guarded than communication within them.

The problem is about coupling. As long as the pieces are truly independent of one another, truly decoupled, then we can parallelize all we want. As soon as there is coupling, there are constraints on the degree to which we can parallelize. The cost of integration is the killer!

Teams of 5 took only one week longer than the teams of 20 people over a 9-month period. So small teams are nearly 4 times as productive, per person, as larger teams. Larger teams produced 5x more defects in their code.

The secret is to build teams and systems that need to coordinate to the minimum possible degree, we need to decouple them. Working hard to maintain this organizational modularity is important and one of the real hallmarks of genuinely high-performing, scalable organizations.

Designing organizations to minimize the coupling between different groups of people is the modern strategy for large, fast-growing companies.

We must retain our ability to change code and systems in one place, without worrying about the impact of those changes elsewhere.

Pull the things that are unrelated further apart, and put the things that are related closer together.

It is a mistake to optimize code to reduce typing. We are optimizing for the wrong things. Code is a communication tool; we should use it to communicate.

The primary goal of code is to communicate ideas to humans!

We should never choose brevity at the cost of obscurity. Optimize to reduce thinking rather than to reduce typing.

If you are really interested in the performance of your code, don’t guess about what will be fast and what will be slow; measure it!

Coupling: Given two lines of code, A and B, they are coupled when B must change behavior only because A changed.

Cohesion: They are cohesive when a change to A allows B to change so that both add new value. Cohesion is about putting related concepts, concepts that change together, together in the code.

Separation of concerns is "One class, one thing. One method, one thing."

How and where we store something is not germane to the core shopping-cart behavior that we are trying to create.

The essential complexity of a system is the complexity that is inherent in solving the problem that you are trying to solve, how to calculate the value of a bank account.

The accidental complexity is everything else—the problems that we are forced to solve as a side effect of doing something useful with computers. These are things like persistence of data.

It is in our interests to work to minimize, without ignoring, accidental complexity.

An effective approach to improving our designs through separation of concerns is to focus very clearly on separating the concerns of the accidental and essential complexities of our systems.

Having an "and" in the description of a class or a method is a warning sign. It says that I have two concerns rather than one.

Try to maintain a very low tolerance for complexity. Code should be simple and readable, and as soon as it begins to feel like hard work, you should pause and start looking for ways to simplify.

Readability is a fundamental property of good code. It has a direct economic impact on the value of that code.

Always translate information that crosses between Bounded Contexts.

If your module, class, or function does more than one thing, your concerns aren’t really separate. The result is that separation of concerns is a fantastic tool to guide us, definitively, in the direction of the design of better software.

As a consumer of a function, class, library, or module, I should not need, or care, to know anything about how it works, only how I use it.

The most efficient software development teams are not fast because they discard quality but because they embrace it. It is the professional duty of a software engineer to recognize this truth and to always offer advice, estimates, and design thoughts based on a high-quality outcome.

Always be thinking of the simplest route to success, not the coolest, not the one with the most tech that we can add to our CVs or résumés.

The problem that people are trying to address when they over-engineer their solutions, by attempting to future-proof them, is that they are nervous of changing their code. In response to that nervousness, they are trying to fix the design in time now, while they are paying attention to it. Their aim is that they won’t need to revisit it in the future. [...] This is a very bad idea.

Design by contract is an approach to software design focused on the contracts, which are specifications that the system, or components of it, support.

Our objective should be to retain our ability to change our mind about implementation, and as far as we can our design, without too much extra work.

We need to build up our instincts to be able to spot design choices that will limit our ability to change our minds later and that allow us to keep our options open.

As soon as we allow third-party code into our code, we are coupled to it. In general, my preference and advice is to always insulate your code from third-party code with your own abstractions.

Microservices are as follows:

Microservices are an organizational scalability play; they don’t really have any other advantage, but let’s be clear, this is a big advantage if scalability is your problem! If you don’t need to scale up development in your organization, you don’t need microservices.

The cost of having one canonical representation of any given idea across a whole system increases coupling, and the cost of coupling can exceed the cost of duplication. This is a balancing act. Dependency management is an insidious form of developmental coupling.

Everything that constitutes releasability is within the scope of your deployment pipeline. If the pipeline says everything is good, there should be no more work to do to make you comfortable to release—nothing…no more integration checks, sign-offs, or staging tests. If the pipeline says it is good, then it is good to go!

The scope of a deployment pipeline should be an independently deployable unit of software.

Professional programming isn’t about translating instructions from a human language into a programming language. Machines can do that. Professional programming is about creating solutions to problems, and code is the tool that we use to capture our solutions.

Software development is not a cost center or a support function; it is the "business."

# development | book-review