Sampul Modern Software Engineering: Doing What Works to Build Better Software Faster
Reading

Modern Software Engineering: Doing What Works to Build Better Software Faster

David Farley

Started: July 1, 2026

65%65%

My Thoughts

I know this book when see someone posted about reading this book in the Linkedin. now days I don't interest with book that contain so many practical code. I'm trying to find book contain methodology, principles, design system or design pattern base on experience Author in software engineering. 

This book said as software engineering we must become expert at learning and expert at managing complexity. we have to learn and continue to learn, then we need to work in ways that allow us to make progress.   

For the author engineering is everything that it takes to make software process, tools, culture - all are part of the whole. 

When the first year I work in my current company. I often wonder why, when preparing to launch an app, the bosses seem to be squandering resources on an MVP. But after that I realize how set an accurate measurement rather than waiting for something bad happen. Of course you can optimize and precise to reduce cost latter.

In the late of afternoon at the office, I amaze with how small the hard disk external now. If you're millennials or generations before me, you will not believe it (try search portable SSD USB 3.2.).

Like Fred Brooks said in 1986, 

We must observe that the anomaly is not that software progress is so slow but that computer hardware progress is so fast. No other technology since civilization began have seen six orders of magnitude price-performance gain in 30 years. 

Now AI show me how fast to make a website or a 3D game in the browser, but I believe it's doesn't change that hardware progress is still so fast.


Things I Learned

  • Engineering
  • Software engineering
  • Continues Delivery
  • Test-Driven Development

My Notes

Chapter 1 Engineering - The Practical Application of Science

So the Author ( David Farley ) made a definition about software engineering that underpins the ideas in this book is this:

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

To become expert at learning, we need the following:

  • Iteration
  • Feedback
  • Incrementalism 
  • Empiricism

To become expert at managing complexity, we need the following:

  • Modularity
  • Cohesion
  • Separation of Concerns
  • Abstraction
  • Louse Coupling

📖 Accretion means the slow process of growth or increase by adding layers or gathering small pieces over time. 

📖 Physicist is a scientist who studies the fundamental laws of the physical universe, focusing on how matter and energy interact. 

Chapter 2 - What is Engineering

Engineering != Code

An Engineer, though, can create a machine to make something smaller and more precise. we can build machines (tools) to make smaller machines. 

"Soak Test" is run the test for a bit longer than the normal time between failure.

The Accuracy Measurement - The engineering focused team will use accurate measurement rather than waiting for something bad happen. They will measure the performance of their software to detect the leaks before they become a problem.

All engineering is a game of optimization and trade-off.  If you make make our system more secure, it will be more difficult to use, if we make it more distributed, we will spend more time integrating the information that it gathers. If we add more people to speed up development, we will increase the communication overhead, coupling and complexity.

Chapter 4 Working Iteratively

Iteration is defined as "a procedure in which repertition of sequence of operations yield results successively closer to a desired result"

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. 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! 

Agile thinking shares this idea with science. Approaching ideas from a skeptical perspective and looking to prove ideas wrong, rather than prove them right (“falsifiability”), are inherent to a
more scientific mindset.

Manifesto for Agile Software Development

  • Individuals and interactions over processes and tools

  • Working software over comprehensive documentation

  • Customer collaboration over contract negotiation

  • Responding to change over following a plan

That is, while there is value in the items on 
the right, we value the items on the left more.

Chapter 5 Feedback

Private note

The contents of this note are not publicly available.

Chapter 6 Incrementalism

Private note

The contents of this note are not publicly available.

Chapter 7 Empiricism

Private note

The contents of this note are not publicly available.

Chapter 8 Being Experimental

Private note

The contents of this note are not publicly available.

Chapter 9 Modularity

Modularity is defined as “the degree to which a system’s components may be separated and recombined, often with the benefit of flexibility and variety in use.”

If we need small teams to efficiently create good, high-quality work, then we need to find ways to seriously limit the coupling between those small teams. This is at least as much an organizational strategy problem as it is a technical one. We need modular organizations as well as modular software.

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.

Take a look at this one:

def create_order(data):
payment = StripePayment()
payment.charge(data["amount"])

This code is difficult to test without actually interacting with Stripe because `StripePayment` is instantiated directly within the function.

If made with a seam:

def create_order(data, payment_service):
payment_service.charge(data["amount"])

In production:

create_order(data, StripePayment())

when testing:

create_order(data, FakePayment())

Here, `payment_service` acts as a seam. We have a point where the implementation can be swapped out without altering the `create_order()` logic.

Chapter 10 Cohesion

Private note

The contents of this note are not publicly available.

Chapter 11 Separation of Concern

Separation of concerns is defined as “a design principle for separating a computer program into distinct sections such that each section addresses a separate concern.”

An extremely useful tool in achieving a good separation of concerns is dependency injection. Dependency injection is where dependencies of a piece of code are supplied to it as parameters,
rather than created by it.

An effective route into improving the quality of our design is to aim to separate concerns in a specific way—that is, to separate the essential complexity of our systems from the accidental. 

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, how to total the items in a
shopping cart, or even how to calculate the trajectory of a spaceship, for example. Addressing this complexity is the real value that our system offers.

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, displaying things on a screen, clustering, some aspects of security…in fact anything that is not directly related to solving the problem at hand.

You can think of the new abstraction as a port, or a vector through which information flows. Whether or not you decide to make the port polymorphic is entirely up to you and the circumstances in your code, but even where you don’t, this code is better. It is better because you have improved the separation of concerns, improved the cohesion by maintaining a more consistent level of abstraction, and improved both its readability and maintainability.

The concrete implementation of this port is an adapter that acts as a translation service, translating ideas from.

We can use the example of user registration + sending a welcome email, but in PHP.

Without separation of concerns, everything is mixed together:

<?php

class RegisterUserService
{
public function register(string $email, string $password)
{
// Concern 1: save the user
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');

$stmt = $pdo->prepare(
'INSERT INTO users (email, password) VALUES (?, ?)'
);

$stmt->execute([
$email,
password_hash($password, PASSWORD_DEFAULT)
]);

// Concern 2: send an email
mail(
$email,
'Welcome',
'Thanks for registering'
);
}
}

The problem is that RegisterUserService now knows too much:

RegisterUserService
|
+-- knows how to connect to the database
+-- knows the SQL used to insert a user
+-- knows how to hash the password
+-- knows how to send an email

We can separate those concerns.
First, create a contract for the repository:

<?php

interface UserRepository
{
public function create(
string $email,
string $password
): User;
}

Then create the database implementation:

<?php

class MySqlUserRepository implements UserRepository
{
public function __construct(
private PDO $pdo
) {}

public function create(
string $email,
string $password
): User {
$stmt = $this->pdo->prepare(
'INSERT INTO users (email, password) VALUES (?, ?)'
);

$hashedPassword = password_hash(
$password,
PASSWORD_DEFAULT
);

$stmt->execute([
$email,
$hashedPassword
]);

return new User(
id: (int) $this->pdo->lastInsertId(),
email: $email
);
}
}

Now separate the email concern as well:

<?php

interface EmailSender
{
public function sendWelcomeEmail(
string $email
): void;
}

And its implementation:

<?php

class PhpMailEmailSender implements EmailSender
{
public function sendWelcomeEmail(
string $email
): void {
mail(
$email,
'Welcome',
'Thanks for registering'
);
}
}

Now the business logic becomes much cleaner:

<?php

class RegisterUserService
{
public function __construct(
private UserRepository $userRepository,
private EmailSender $emailSender
) {}

public function register(
string $email,
string $password
): User {
$user = $this->userRepository->create(
$email,
$password
);

$this->emailSender->sendWelcomeEmail(
$email
);

return $user;
}
}

The service can be created like this:

<?php

$pdo = new PDO(
'mysql:host=localhost;dbname=app',
'root',
''
);

$userRepository = new MySqlUserRepository($pdo);

$emailSender = new PhpMailEmailSender();

$service = new RegisterUserService(
$userRepository,
$emailSender
);

$service->register(
'doni@example.com',
'secret'
);

This is where dependency injection happens.

Notice this part:

new RegisterUserService(
$userRepository,
$emailSender
);

RegisterUserService does not create these objects by itself:

new MySqlUserRepository(...)

or:

new PhpMailEmailSender()

Instead, the dependencies are provided from the outside.

The flow looks like this:

main / controller
|
+-- create MySqlUserRepository
|
+-- create PhpMailEmailSender
|
v
RegisterUserService
|
+-- UserRepository
|
+-- EmailSender

When this is called:

$service->register(
'doni@example.com',
'secret'
);

the execution flow becomes:

RegisterUserService::register()
        |
        v
$userRepository->create()
        |
        v
MySqlUserRepository::create()
        |
        v
MySQL


RegisterUserService
        |
        v
$emailSender->sendWelcomeEmail()
        |
        v
PhpMailEmailSender
        |
        v
mail()

The important part is that RegisterUserService depends only on interfaces:

private UserRepository $userRepository;
private EmailSender $emailSender;

It does not care whether the actual implementation is:

MySqlUserRepository
PostgresUserRepository
ApiUserRepository
FakeUserRepository

The same applies to email:

PhpMailEmailSender
SendGridEmailSender
SesEmailSender
FakeEmailSender

For example, if we later switch from PHP mail() to SendGrid:

<?php
class SendGridEmailSender implements EmailSender
{
    public function sendWelcomeEmail(
        string $email
    ): void {
        // Call the SendGrid API
    }
}

We only need to change the wiring:

$emailSender = new SendGridEmailSender();
$service = new RegisterUserService(
    $userRepository,
    $emailSender
);

RegisterUserService does not need to change.
This is also useful for testing.

<?php
class FakeEmailSender implements EmailSender
{
    public array $sentEmails = [];
    public function sendWelcomeEmail(
        string $email
    ): void {
        $this->sentEmails[] = $email;
    }
}

Then:

$fakeEmailSender = new FakeEmailSender();
$service = new RegisterUserService(
    $fakeUserRepository,
    $fakeEmailSender
);
$service->register(
    'doni@example.com',
    'secret'
);
assert(
    in_array(
        'doni@example.com',
        $fakeEmailSender->sentEmails
    )
);

So the mental model is:

Separation of Concerns
        ↓
separate:
business logic
database
email

Dependency Injection
        ↓
provide dependencies from the outside

Interface / Port
        ↓
define the contract

Adapter
        ↓
provide the concrete implementation

A simple PHP project structure could look like this:

src/
├── Application/
│   └── RegisterUserService.php
│
├── Domain/
│   └── User.php
│
├── Port/
│   ├── UserRepository.php
│   └── EmailSender.php
│
└── Adapter/
    ├── MySqlUserRepository.php
    └── PhpMailEmailSender.php

Chapter 12 Information Hiding and Abstraction

Private note

The contents of this note are not publicly available.

← Back to My Library