Short Summary
In Modern Software Engineering: Doing What Works to Build Better Software Faster, David Farley outlines practical methods to build reliable, maintainable software quickly. He highlights key practices such as Continuous Delivery and Test-Driven Development (TDD), with a strong emphasis on feedback loops and iterative improvement. Farley argues that TDD is central to creating robust systems, as it ensures software is built incrementally and verified at every step, reducing errors and increasing confidence in the code.
Farley focuses on the importance of feedback throughout the development process, from design to deployment. By embedding automated tests and using Continuous Delivery pipelines, teams can get fast, reliable insights into the health of their systems. He promotes a scientific approach to software engineering, encouraging teams to experiment, measure results, and refine their processes based on evidence. This focus on TDD and feedback makes the book a valuable resource for those looking to improve the quality and efficiency of their software development practices.
Key Notes and Takeaways
Stability and Throughput Measurement (p. 33, 34)
Measure of stability and throughput are each tracked by two measures.
Category | Metric | Definition |
---|---|---|
Stability | Change Failure Rate | The rate at which a change introduces a defect at a particular point in the process. |
Recovery Failure Time | How long to recover from a failure at a particular point in the process. | |
Throughput | Lead Time | A measure of the efficiency of the development process. How long for a single-line change to go from “idea to working software”. |
Frequency | A measure of speed. How often are changes deployed into production. |
Stability measurement is a measure of the quality of work done.
Throughput is a measure of a team’s efficiency at delivering ideas, in the form of working software.
Iteration (p. 43)
Iteration is defined as “procedure in which repetition of a sequence of operations yields results successively closer to a desired result.
This definition reminds us that iteration allows us to progressively approach some goals even we don’t really know how to approach our goals.
As long as we have some way of telling whether we are closer to, or further from, we could even iterate randomly and still achieve our goal.
This is in essence how evolution works and also at the heart of how modern machine learning (ML) works.
Discard Waterfall Thinking (p. 44)
Waterfall-style thinking starts from the assumption that “if we only think/work hard enough, we can get things right at the beginning”.
Agile thinking invert this, it starts from assumption that we will inevitably get things wrong. We won’t understand what users want, we won’t get the design right straight away, we won’t know if we have caught all the bugs in the code that we wrote, and so on. Because they start off assuming that they will make mistakes, agile teams work in a way that, quite intentionally, mitigates the cost of mistakes.
Agile thinking approaches ideas from a skeptical perspective and looking to prove ideas wrong, rather than prove them right, are inherent to a more scientific mindset.
Always Have a Software in Releasable State (p. 54, 62)
CI is the practice of merging all developer’s working copies to a shared mainline several times a day.
In CI we are going to commit our changes frequently, multiple times per day. This means that each change needs to be atomic, even if the feature that it contributes is not yet complete. This gives us more opportunities to learn and to understand if our code still works alongside everyone else’s.
Instead of working on a feature until it is “finished” or “ready for production”, continuous integration and continuous delivery demand of us to make changes in small steps and have some ready for use after every small step.
Port & Adapter Pattern (p. 76, 147)
At any interface point between two components of the system that we want to decouple, a port, we define a separate piece of code to translate inputs and outputs, the adapter. This allows us more freedom to change the code behind the adapters without forcing change on other components that interact with it through this port.
Always communicate between services using Ports & Adapters.
Empiricism (p. 85)
If we want to maximise the rate at which an algorithm make progress, we should try to keep as much work as possible on a single thread, unless we can make progress and never join the results back together again.
TDD Step by Step (p. 100, 190)
I thought about and characterised the problem. “I have decided on the behaviour that I want of my system and captured it as a test case.”
- I formed a hypothesis. “I expect my test to fail!”
- I made a prediction. “When it fails, it will fail with this error message…”
- I carried out my experiment: “I ran the test”.
Example: Car vs. BetterCar (p. 190)
// Car (not testable)
public class Car {
private final Engine engine = new PetrolEngine();
public void start() {
putIntoPark();
applyBrakes();
this.engine.start();
}
private void applyBrakes() {}
private void putIntoPark() {}
}
@Test
public void shouldStartCarEngine() {
Car car = new Car();
car.start();
// Nothing to assert!
}
// BetterCar (testable)
public class BetterCar {
private final Engine engine;
public BetterCar(Engine engine) {
this.engine = engine;
}
public void start() {
putIntoPark();
applyBrakes();
this.engine.start();
}
private void applyBrakes() {}
private void putIntoPark() {}
}
@Test
public void shouldStartBetterCarEngine() {
FakeEngine engine = new FakeEngine();
BetterCar car = new BetterCar(engine);
car.start();
assertTrue(engine.startedSuccessfully());
}
The first “Car” example is not testable because we can’t see the effect of starting the car.
The second example “Better Car” use dependency injection to improve separation of concerns and cohesion because now the class is no longer interested in how to create a petrol engine. As a result, the code is more testable and flexible.
Cohesion (p. 133)
Cohesion in human systems
High performing team has ability to make their own decisions without the need to ask permission of anyone outside the team. The team has all that it needs within its bounds to make decisions and to make progress.
Cohesion with TDD
We create a test case before we write the code that describes the behaviour that we aim to observe in the system. This allows us to focus on the design of the external API/interface to our code. If we write too much code, more than is needed to meet the specification, we are cheating our development process and reducing cohesion of the implementation. If we write too little, then the behavioural intent won’t be met.
Better Software Faster (p. 154, 156)
There should be no trade-off between speed and quality. Managers and organisations want better software faster, not worse software faster.
Abstraction or information hiding is clearest route to habitable systems. One of the tendencies that we should be aware of and guard against, is chasing technically shiny ideas.
We should always be thinking of the simplest route to success, not the coolest and within economic constraints. Avoid future-proofing. Spot the bad sign such as “We may not need this now, but we probably will in the future”.
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.
No Low-Code Development (p. 156)
One common approach to diagram-driven or low-code development is to use the diagram to generate source code. The idea here is to use the diagrams to create the broad structure of the code, and then the detail can be filled in.
The strategy is pretty much doomed to a failure by one difficult-to-solve problem. The problem is that you are almost always going to learn more as the development of any complex system evolves. At some point, you will need to revisit some of your early thinking.
Don’t Apply DRY on Two Microservices (p. 180)
If you are creating a microservice-based system, with each service being independently deployable, and each service having its own deployment pipeline, you should not apply DRY between microservices. Don’t share code between microservices.
What’s Next
After finish reading the book, I recommend to further read:
- Mythical Man-Month, The: Essays on Software Engineering
- Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations
- Domain-Driven Design: Tackling Complexity in the Heart of Software