Introduction

I made an effort to read A philosophy of software design, which is written in English, so here are my notes.

About A philosophy of software design

Who is John Ousterhout

Chapter 1 It's all about complexity

An overview of what follows. (Honestly, I think it is fine to skip this.)

  • The biggest constraint in software is "complexity" As features increase, dependencies increase, making the software harder to understand and modify, and lowering development speed and quality.

  • Complexity inevitably increases over time Despite the efforts of developers and tools, complexity keeps accumulating.

  • Two approaches to complexity

    1. Simplification: make code clear and reduce special cases.
    2. Encapsulation: localize complexity through modular design.
  • The limits of waterfall

    • Unlike physical systems, software cannot be fully understood from the initial design alone, so the waterfall approach amplifies problems.
  • The importance of iteration

    • Repeat design → implementation → evaluation in small cycles.
    • Problems are fixed early, and experience can be applied to the next cycle.
  • Conclusion → An iterative (agile) approach is essential in software development.

Chapter 2 The Nature of Complexity

This chapter mainly categorizes and defines complexity.

Complexity exists regardless of the size of a system, and a large system is not necessarily complex.

C = Σ cₚtₚ

The complexity of the whole system (C) is determined by the sum, over each part p, of that part's complexity (cₚ) multiplied by the fraction of time developers spend on that part (tₚ).

  • Three symptoms of complexity

    1. Change amplification: a small change requires many modifications.
    2. Cognitive load: the amount of knowledge needed to perform a task is large, making learning and understanding heavy.
    3. Unknown unknowns: it is not even clear what needs to be modified or what information is needed.
  • Two causes of complexity

    1. Dependencies: code is strongly tied to other code.
    2. Obscurity: important information is not made explicit.
  • Complexity increases incrementally Systems become uncontrollable not through big failures but through the accumulation of small dependencies and obscurities.

  • Conclusion Complexity makes modifying software difficult and risky. → In design, the top priority is to "reduce dependencies and avoid obscurity."

Chapter 3 Tactical vs. Strategic Programming

  • Problems with tactical programming

    • An attitude of "just get something working quickly."
    • It looks efficient in the short term, but it piles up complexity and increases technical debt.
    • The "tactical tornado" (a person who churns out lots of code but leaves confusion behind) is introduced as a typical example.
  • The importance of strategic programming

    • Prioritizes the long-term health of the system design over short-term speed.
    • A mindset of "investing" in design improvement is needed.
    • With continuous small investments (10-20% of development time), development speed improves in the long run.
  • The essence of technical debt

    • The tactical approach is an act of "borrowing time from the future."
    • You gain short-term results at the cost of long-term development speed.
  • The dilemma in startups

    • Pressure for early release tends to make people tactical.
    • Facebook grew with "Move fast and break things," but later shifted its policy to "Move fast with stable infrastructure."
    • There are also success stories, such as Google and VMware, that took a strategic approach from the start.
  • Conclusion

    • Good design cannot be obtained without continuous investment.
    • By accumulating small investments, you prevent the buildup of large problems and end up recouping the cost.
    • It is most effective when every engineer makes "small design improvements" as a daily habit.

Chapter 4: Modules Should Be Deep

  • The purpose of module design

    • Minimize the complexity developers face.
    • Reduce dependencies between modules and hide internal complexity.
  • Interface and implementation

    • Interface = the specification exposed to the outside.
    • Implementation = the code that realizes the interface.
    • Users only need to understand the interface, and the internal complexity is invisible.
  • The role of abstraction

    • Omit unnecessary details and show only the important information.
    • The more unnecessary information you cut, the better, but hiding important information leads to a "false abstraction."
    • Examples: file system APIs, the controls of a microwave or a car.
  • Deep modules

    • Provide powerful functionality through a simple interface.
    • Because they hide large amounts of internal complexity, users only need to deal with a small amount of information.
    • Examples: Unix file I/O, garbage collectors.
  • Shallow modules

    • The interface is complex relative to the functionality.
    • The benefit of abstraction is small and it does not help reduce complexity.
    • Examples: a linked-list class, small methods split up pointlessly finely.
  • Classitis

    • The phenomenon of over-splitting classes because of the misconception that "smaller classes are better."
    • As a result, interfaces multiply and the complexity of the whole system grows.
    • What is truly valuable is the "deep class."

Chapter 5 Information Hiding (and Leakage)

1. Information Hiding

  • The most important technique for making modules "deep."
  • Hide internal implementation details (algorithms and data structures) and expose only abstract operations as the interface.
  • Benefits:
    1. Simpler interfaces → lower cognitive load
    2. Localized changes → internal changes do not ripple out to other modules

Example: even if the internal implementation of the TCP protocol is changed, the send/receive API can be used as is.

2. Information Leakage

  • The opposite of information hiding: the same knowledge appears in multiple places.
  • Problems:
    • It creates dependencies between modules
    • Changes ripple out to multiple places
  • In particular, "knowledge leaking into the interface" is a Red Flag.

Example: knowledge of a file format is spread across multiple classes → when the format changes, both must be modified.

3. Temporal Decomposition

  • An anti-pattern of designing modules based on the order in which processing is executed.
  • As a result, the same information is duplicated across multiple classes, causing information leakage.

Example: if you split HTTP request handling into separate classes for "reading" and "parsing," both hold knowledge of the request format and dependencies increase.


4. A real example (HTTP server)

  • Bad design: returning an internal data structure (e.g., a Map) as is → implementation details leak.
  • Good design: hide type conversion as in getIntParameter, providing callers with only abstract operations.
  • An example of partial information hiding: providing default values. Callers do not need to know internal details.

5. Red Flags

  • Information Leakage: knowledge is duplicated in multiple places.
  • Temporal Decomposition: the processing order is turned directly into modules.
  • Overexposure: to use a commonly used feature, you have to learn unnecessary details.

6. Going too far with information hiding

  • It is not OK to hide even information that is needed externally.
  • What should be hidden is only "internal information that is unnecessary externally."

7. Conclusion

  • Deep module = a module that hides a lot of information and provides powerful functionality through a simple interface.
  • Information leakage amplifies complexity, so it should always be avoided.
  • When splitting into modules, focus on "localization of knowledge," not "order of operations."

My thoughts and notes

What this has in common with encapsulation in OOP:

  • Not showing internal details to the outside
  • Making it possible to localize changes
  • The idea of "keep it simple from the outside; the inside can be complex"

However, encapsulation in OOP targets individual classes, whereas the Information Hiding discussed in this book is one level more abstract and targets the module level.

It concerns things like APIs, network protocols, and library interfaces, that is, collections of classes and the parts close to system boundaries.

  • Encapsulation (OOP): do not let outsiders manipulate an Entity directly. Do not expose internal state, to prevent side effects.
  • Information Hiding: by defining the interface of a module or API, you avoid leaking the complexity of the "chunk" to the outside and make it look simple.

By limiting what can be operated through the interface, you can lower the cognitive load of users (developers) and also limit the scope of impact of changes.

For example, an interface consolidated into 5 methods is easier to understand than one with 100 methods, and its impact scope is smaller. As a result, test code is also easier to write.

RESTful design follows the same idea: the state of a resource is manipulated only indirectly through a limited set of operations (HTTP methods), which hides the complexity of the whole system.

Depending on the system's specifications, there are times when you inevitably have to build complex logic. Even then, by dividing it into modules you can localize the complexity and keep it from affecting other parts.

Chapter 6 General-Purpose Modules are Deeper

  • Specialization invites complexity: overly specialized code becomes complex and harms maintainability and understandability.
  • General-purpose APIs produce deep modules: by making a common mechanism handle a wide range of cases, you can simplify the interface and promote information hiding.
  • "Somewhat general-purpose" is ideal: too much specialization makes things complex, and conversely too much generality makes things hard to use. A "somewhat general-purpose" design is the balanced solution.
  • Eliminating special cases: adding special cases leads to code full of if statements and increases bugs and complexity, so it is important to design the normal case so that it handles them naturally.
  • Push specialization up and down: by isolating specialized processing in appropriate places such as the UI layer or the device driver layer, you can keep the general-purpose parts of the lower layers clean.

This chapter explains how to deal with complexity in software design, centering on the relationship between "generality" and "specialization." The author considers excessive specialization the biggest cause of complexity, and argues conversely that general-purpose code is simple, easy to understand, and highly maintainable.

When designing modules and classes, it is recommended to design APIs to be as general-purpose as possible. In the text editor example, for instance, rather than having many special-purpose methods such as backspace and delete, consolidating them into general-purpose methods such as insert and delete(Position start, Position end) enables a simpler, more reusable design.

However, excessive generality to the point of "can do anything" is also counterproductive and makes the interface hard to use. Therefore, a "somewhat general-purpose" design, which responds to present needs while anticipating future reuse, is said to be the optimal solution.

Also, it is recommended to eliminate "special cases," a contributor to complexity, as much as possible and to design the normal case so that it handles them naturally. For example, rather than treating the "no selection" state specially, always treating it as an "empty selection" removes unnecessary if statements.

Furthermore, specialization that is truly necessary should be pushed into the layers above and below, such as the UI or device drivers, and it is important not to let it mix into the general-purpose foundation.

Ultimately, the conclusion is that by reducing unnecessary specialization and separating it from general-purpose code, you can achieve deep class design, better information hiding, and simple, clear code.

Notes

The book wrote about editor design here, but I will think about it with other examples myself.

Chapter 7 Different Layer, Different Abstraction

  • Each layer should have a different abstraction: each class or layer needs to provide a clear abstraction that differs from those above and below it.
  • Pass-through methods/variables are red flags: things that add no new functionality and simply relay processing or data indicate a design problem.
  • Clear division of responsibility is the key: when responsibilities between classes are ambiguous, redundant interfaces and dependencies arise, making the system shallow and complex.
  • Exceptionally acceptable cases: where pass-through is essential to the role, as with dispatchers and decorators, it can be used appropriately.
  • Direction for refactoring: class decomposition should be improved by redistributing responsibilities, exposing things directly, merging, and so on.

Chapter 7 focuses on the role of abstraction in each layer in software design. In a good design, each layer provides a different abstraction and clearly shares responsibility. However, when adjacent layers have similar abstractions, design problems arise.

Typical examples are pass-through methods and pass-through variables. They make classes shallow and add complexity without adding functionality. They also strengthen dependencies between classes and make the design fragile to change.

The solution is to sort out responsibilities through refactoring. Methods include:

  • Expose the lower-level class directly (eliminate the relay).
  • Redistribute responsibilities between classes.
  • Merge classes if necessary.

On the other hand, not all duplication of interfaces is bad.

  • Dispatcher: distributes to multiple handlers based on arguments.
  • Decorator: wraps an existing class to add logging, measurement, access control, and so on.

In these cases, the pass-through plays a meaningful role.

In other words, each class should have its own abstraction and responsibilities differentiated from those above and below it, and if not, the design needs to be reviewed.

Chapter 8 Pull Complexity Downwards

  • Complexity should be handled inside the module: rather than pushing it on users, absorbing it within the module lets you simplify the interface.
  • A simple interface matters more than a simple implementation: it is more valuable to make the user experience simple even if developers have a harder time.
  • The character-oriented vs. line-oriented example: a text management class can make the UI side simpler by handling text per character rather than per line.
  • Configuration parameters push complexity onto others: instead of throwing hard decisions to users, developers should decide the best approach.
  • Beware of going too far: cramming everything into a single class is counterproductive. Only the parts closely related to the functionality need to have their complexity reduced.

Chapter 8 deals with the question of where complexity should be handled. The principle is "handle it inside the module rather than pushing it on the user." There are few module developers but many users. Therefore, even if developers take on extra work, it is more important to reduce the burden on users.

The example given is a GUI text editor. If the interface operates per line, the UI side has to handle the complexity of splitting and joining. If the interface is per character, on the other hand, the internal implementation becomes complex, but the UI becomes much simpler. Also, configuration parameters seem convenient at first glance, but in reality they leave design difficulties to the user. It is preferable for developers to make appropriate design decisions internally.

However, taking this idea too far leads to an unreasonable design such as "cram all functionality into one class." Complexity should be pulled downward only when it relates to the module's essential functionality.

In short, "for a module, a simple interface is more important than a simple implementation," which reduces the cognitive load on users and achieves a design that is clean and easy to understand for the system as a whole.

Chapter 9 Better Together Or Better Apart?

  • The decision to combine or separate is fundamental to design: whether to group or split classes and modules is directly tied to a system's complexity and modularity.
  • Splitting risks increasing complexity: the more parts there are, the harder they are to manage, and interfaces and duplication also increase.
  • Conditions where combining is effective: when there is shared information, simultaneous use, conceptual overlap, or interdependence in understanding.
  • Duplication is a red flag: if functionality or knowledge is duplicated, it should be combined.
  • The depth of abstraction is the criterion: if combining produces a "deep abstraction," it is good design; conversely, if it is shallow and complex, they should be separated.

Chapter 9 deals with the question in software design of "whether to combine or separate code." Splitting makes each part look simple, but the increase in the number of parts creates new complexity. Interfaces increase, knowledge gets duplicated, and management becomes harder.

Combining is effective when two pieces of code share information, are used together, overlap conceptually, or one is needed to understand the other. "Duplication" in particular is a strong sign that they should be combined.

However, combining unrelated functionality makes a class shallow and harder to understand. Conversely, appropriate combination makes a module "deep," simplifies the interface, and concentrates complexity in a good way.

Examples include HTTP request parsing and file system buffering. Whether these are combined or separated greatly changes the depth of the design and the distribution of complexity.

The ultimate goal is to reduce the complexity of the whole system and design "deep classes" with clean, simple abstractions.

Chapter 10 Define Errors Out of Existence

  • Exception handling is a major source of complexity in software.
  • Exception handling code is harder to write, harder to read, and more error-prone than normal-case code.
  • The best strategy is to "design so that exceptions do not occur."
  • When exceptions are unavoidable, they should be handled in a way that minimizes their impact on the complexity of the whole system.

Chapter 10 discusses how exception handling makes code complex. Exceptions disrupt the normal control flow, prevent operations from completing, and often generate new exceptions during recovery. As a result, exception handling code becomes long, hard to read, and a breeding ground for bugs.

There are mainly three strategies for exception handling:

  1. Pass the buck: report the exception to a higher level.
  2. Crash: terminate the program (or part of it).
  3. Mask: handle the exception locally and hide it.
But the ideal is to "eliminate exceptions from the design." For example, in substring search, instead of "-1 if not found" or "throw an exception," defining it to "return the end of the string + 1" makes the error condition itself unnecessary. Similarly with iteration, instead of throwing an exception when elements run out, you can provide a hasNext method.

By working on the semantics of an operation so that "all outcomes are treated as normal behavior," many exceptions become unnecessary, and the whole system becomes easier to understand and use.

Chapter 11 Design it Twice

  • The first design idea is unlikely to be the best.
  • By considering multiple alternatives, the essence of the problem becomes clear and you can find better solutions.
  • New ideas often emerge in the process of comparing alternatives.
  • The principle of "design it twice" is effective at every level, from class design to whole-system design.
  • Trying multiple designs also improves your design skills themselves.

Chapter 11 emphasizes the importance in software design of "not relying on your first idea." By creating and comparing at least two different ideas rather than considering only one,

  • the strengths and weaknesses of each
  • ease of use for users
  • generality and efficiency

and so on come to light.

For example, in designing a text management class for a GUI text editor, by considering different API designs such as

  • line-oriented
  • character-oriented
  • range-oriented

you find that none is perfect, and that a range-oriented API is actually the most suitable.

The process of comparing alternatives and finding problems itself leads to discovering new designs. Also, "designing it twice" not only yields the best design but also contributes to the designer's own growth.

Chapter 12

Four excuses for not writing comments, and rebuttals

  1. Good code is self-documenting

    • Clean code is easy to understand, but it can only show low-level information.
    • Comments provide higher-level abstractions, and both are needed.
  2. I don't have time to write comments

    • If you put it off, it never gets written in the end.
    • Comments are an investment in maintainability and improve efficiency.
  3. Comments get outdated and mislead

    • Updates are minor and can be caught in code review.
    • If you avoid duplication and place comments near the code, the problem is small.
  4. The comments I've seen so far were worthless

    • Indeed, many are mediocre, but they can be improved if you learn the right way.

Benefits of good comments

  • They record information in the designer's head and supplement intent that cannot be expressed in code.
  • They improve the work efficiency of future developers and yourself, and prevent misunderstandings and bugs.
  • They reduce cognitive load and reduce unknown unknowns.
  • They clarify dependencies and obscurity, and manage complexity.

Contrast with Robert Martin

  • Martin: "Comments are a compensation for failure, a necessary evil"
    • He argues that things should be expressed in code (e.g., long method names) rather than comments.
  • The stance of this book:
    • Comments are not a failure; they provide information that cannot be expressed in code.
    • Code and comments have different roles, and both are indispensable.

Conclusion

  • Comments are not "a chore" or "a failure."
  • They are a fundamental element that complements abstraction, manages complexity, and raises software quality.

Chapter 13 Summary: Write "information that cannot be seen from the code" in comments

  • Principle: comments explain "what is not obvious from the code" (intent, background, conventions, abstractions).
  • Making abstractions visible: code is low-level. Abstractions (design ideas, boundaries, contracts) are defined in comments.
  • Precision and intuition: complement the code from both directions: low-level (supplying precision) and high-level (showing intent and reasons).
  • Emphasis on interfaces: clarify the external behavior of classes/methods (arguments, return values, constraints, side effects).
  • Validating the design: being unable to write a comment, or being forced to write implementation details, is a sign of a shallow abstraction (a hint for improving the design).

Types of comments and their priority

  • Interface comments (most important): what a class/method "does." From the user's viewpoint. Do not write the implementation.
  • Data structure members: the meaning of a field, units, inclusive/exclusive boundaries, the meaning of null, responsibility (release/close).
  • Implementation comments: before major blocks or loops inside a long method, at a high level, what/why.
  • Cross-module comments: make explicit design decisions that span multiple modules (such as protocol boundaries).

NG: repeating the code

  • Explanations that merely trace the code, such as "add a horizontal scroll bar," are worthless.
  • Good example: put into words premises and intent that are hard to read from the code, such as maxPos.

Examples of information to write at a low level (precision)

  • Units (ms/bytes/characters, etc.)
  • Boundaries (inclusive/exclusive)
  • The meaning of null (state/unset/missing)
  • Ownership and responsibility (who releases the resource)
  • Invariants (e.g., "the list always has at least one item")

Variable comments should focus on nouns (meaning), not "verbs (operations)."


What to write at a high level (abstraction/intent)

  • The purpose of a code fragment and why it is needed (the reason for processing that looks like a detour)
  • The big picture of the processing (e.g., "append the current key to the existing unsent RPC")
  • This lets readers infer the details and prevents unnecessary deletion or modification.

Requirements for interface comments

Methods

  • Explain the external behavior in one sentence at the top (abstraction).
  • A precise definition of arguments/return values (constraints, interdependencies, boundaries).
  • State side effects explicitly (internal state updates, file system writes, caches, etc.).
  • Do not write the implementation algorithm.

Classes

  • What the class can do and what each instance means.
  • The usage model (e.g., an iterator-style interface).
  • Important constraints (e.g., single-threaded) and assumptions.
  • If necessary, only specifications related to abstraction, such as concurrent behavior and externally visible behavior on failure.

IndexLookup example: write the range-search abstraction, key comparison rules, whether concurrent requests are supported, and behavior on failure. Server-to-server message formats and internal data structures are implementation details, so do not write them.


Tips for implementation comments

  • Before major blocks in a long method, note "what this chunk does."
  • Before a loop, note in 1-2 lines what happens in each iteration.
  • Prioritize "what and why" over "how."

Checklist (for applying in practice)

  • Is this information not obvious from the code?
  • Have I avoided mixing abstraction (external behavior) with implementation details?
  • Do variable comments cover units/boundaries/null/responsibility/invariants?
  • Do method comments include constraints, dependencies, and side effects?
  • Have I placed high-level heading comments on long processing?
  • Have I documented design decisions that span modules somewhere?

Conclusion

Comments are a design device for complementing the low-level nature of code and externalizing abstractions, intent, and conventions. By thoroughly "writing the abstraction, not the implementation," you reduce cognitive load and unknown unknowns, making maintenance faster and safer.

Chapter 14 Choosing Names

  • Good names are a form of documentation, aiding understanding and making error detection easier.
  • Bad names create complexity and misunderstanding and cause bugs.
  • The wider the scope, the more descriptive; the narrower, the shorter it may be.
  • It is important to balance specificity and generality.
  • Keeping consistency avoids confusion.

Detailed summary

Naming tends to be underestimated in software design, but it greatly affects whether you can manage complexity. Good names make code self-documenting and make understanding and maintenance easier, while bad names create ambiguity and can lead directly to serious bugs.

Key guidelines

  1. Naming according to scope

    • For a local variable, something short like i is fine.
    • For classes or wide scopes, a descriptive, longer name is needed.
  2. Balance between specificity and generality

    • A clear name such as checksum is desirable.
    • Avoid overly generic names such as tmp or data.
    • However, names that are too specific may limit how they can be used.
  3. Name length

    • Long enough to ensure clarity, but not verbose.
    • A short name is fine if the role is small, but make it descriptive if it is important.
  4. Consistency

    • Use the same name for the same concept.
    • Also unify naming conventions (camelCase, snake_case, etc.).

Conclusion

  • Naming is not just labeling but part of the design itself.
  • The keys are being descriptive at an appropriate length, balancing specificity and generality, and keeping consistency.
  • Good names simplify the system, and bad names invite complexity and bugs.

Chapter 15

  • If you put off comments, their quality is low and they are likely to remain unwritten.
  • Writing comments first makes them part of the design process.
  • Writing comments early raises design quality and lets you validate abstractions.
  • Comments are a "canary for complexity" that indicates whether the design is good or bad.
  • Writing comments first makes the work more enjoyable and also raises overall development efficiency.

Detailed summary

Many developers put off comments and documentation, but as a result,

  • they remain unwritten
  • the design intent is forgotten and quality drops
  • comments become a mere repetition of the code

and other problems occur.

The author recommends the approach of "writing comments first."

  1. For a new class, first write the interface comment.
  2. Write the signatures and interface comments of the main methods (leaving the bodies empty).
  3. Write the declarations and comments of instance variables.
  4. When filling in the implementation, also write a comment first each time a new element appears.

This way, when the code is complete, the comments are complete too, and nothing is left unwritten.

Furthermore, comments function as a design tool.

  • By putting abstractions into writing, you can evaluate the validity of the design early.
  • If a long, complex comment is needed, it is a sign that the abstraction or decomposition is insufficient.
  • If it can be expressed with a simple comment, the design is also simple and deep.

Also, writing comments early adds enjoyment. The process of putting the design into words is creative, and the simpler you can express it, the greater the sense of accomplishment.

Finally, regarding the objection "won't writing comments first increase the cost of modifications?", the author denies it.

  • Writing comments is only a small fraction of total development time.
  • Writing comments first stabilizes the abstractions and is likely to reduce modifications instead.
  • As a result, overall efficiency actually improves.

Conclusion

  • Writing comments first improves the quality of documentation, the quality of design, and the enjoyment of development.
  • Try continuing until you get used to it, and see how it affects your own development.
  • Comments are not "something to add later" but a process that supports the design itself.

Chapter 16

  • Development is iterative and incremental. Every change makes the design better or worse.
  • Approach it strategically (design first, refactor if necessary), not tactically (getting by with minimal changes).
  • The ideal is to get closer to "the design you would have had if you had taken that specification into account from the start."
  • A change that does not improve the design usually worsens it.
  • Place comments near the code, avoid duplication, and keep them in the code, not in commit logs.
  • Prioritize high-level comments (explanations of overall strategy and abstractions rot less and are more valuable).
  • Before committing, look over the diff to check that comments are in sync.

Practical checklist

  1. Confirm the change approach

    • Do not settle for the smallest immediate diff; review the design as a whole.
    • If necessary, refactor first, then add or fix functionality.
  2. Comment practice

    • Position: directly above or right next to the target code (near the method body, next to the variable declaration).
    • Granularity: the method's strategy at the top, and details just before each block for each phase.
    • No duplication: consolidate design decisions in one place, and use reference comments elsewhere ("see X for details").
    • External information: if an external document already exists, just reference it and do not restate it.
    • Commit logs: important background must be written in the code (the log may copy it, but the code is primary).
  3. Quality gate (pre-commit)

    • Scan the change diff and remove comment inconsistencies and TODO/debug leftovers.
    • Check whether the change has broken any abstractions (comments becoming unnaturally long is a sign of a bad abstraction).

Key ideas

  • Investment mindset: a small investment in refactoring is recouped in future development speed.
  • Comments are a canary for complexity: if it cannot be explained without length, the design or decomposition is wrong.
  • High-level > low-level: as in the binary search example, describe the algorithm and intent rather than line-by-line explanations of the procedure.

Conclusion

  • Change is an opportunity: improve the design a little each time.
  • Think strategically, refactor first when necessary, and manage comments consistently next to the code.
  • This accumulation leads to a system that is readable, easy to change, and has few bugs.

Chapter 17

  • Consistency is the strongest lever for reducing complexity and clarifying behavior. Do similar things in similar ways, and different things in different ways.
  • Without consistency, learning costs, misreadings, and wrong guesses increase. With consistency, known patterns work elsewhere too, and speed and accuracy improve.

Examples of where consistency appears

  • Naming: the same word for the same concept, different words for different concepts. (See Ch. 14 for details.)
  • Coding style: indentation, braces, declaration order, naming conventions, restrictions on dangerous features.
  • Interfaces: once you learn the common surface of multiple implementations, understanding other implementations is faster.
  • Design patterns: adopting a general solution speeds up implementation and is clear to readers.
  • Invariants: properties that always hold reduce special cases and make reasoning easier.

Ways to ensure consistency

  1. Document it
    • Compile important conventions and put them in a prominent place in the project wiki.
    • Write local conventions (such as invariants) at the appropriate place in the code.
    • Starting from an existing published style guide is also effective.
  2. Enforce with automation
    • Use checkers/hooks to block convention violations before commit.
    • Example: detect and fix mixed line endings with a pre-commit hook.
  3. Code review
    • Pointing out details raises the speed of learning conventions and the cleanliness of the code.
  4. When in Rome...
    • Observe the conventions of existing files (public/private order, naming case, ordering) and follow them.
    • Look for and match similar design decisions that already exist.
  5. Do not change existing conventions on your own
    • A "better idea" alone is not enough.
    • If changing:
      • Is there important new information that did not exist then?
      • Is it good enough to be worth migrating everything?
    • With organizational agreement, carry it out to the point where no trace of the old convention remains.

Beware of overdoing it

  • Do not force different things into the same mold (reusing inappropriate variable names, forcing ill-fitting patterns).
  • The value of consistency arises only when you can trust that "if it looks like x, it really is x."

Practical checklist

  • The same name for the same concept / different names for different concepts.
  • Consolidate and document style, naming, and invariants in one place.
  • Automated checks: make lint, formatter, and pre-commit hooks mandatory.
  • Educate through review: give specific examples and grounds (convention links) with comments.
  • New code should follow the existing way of doing things.
  • Convention changes require agreement plus a full migration plan. Avoid partial adoption.

Conclusion

  • Consistency is an investment: with the small costs of establishing conventions, automation, review, and following existing practice, you get large returns in readability, ease of change, and fewer bugs.
  • This chapter evaluates recent trends along the axis of "can it minimize complexity?"
  • Key principles: quality (depth) of abstraction / information hiding / suppressing change amplification / consistency.

Key points and evaluation by trend

1) Object-oriented programming and inheritance

  • Interface inheritance: reusing the same interface for many purposes → reduces learning cost and increases depth, which helps against complexity.
  • Implementation inheritance: reduces duplication by sharing default implementations, but tends to cause strong coupling and information leakage between parent and child, and increases the cost of understanding the whole hierarchy.
    • If you use it, be careful: consider composition first. Manage the parent's state entirely within the parent (children are read-only or go through parent methods).

2) Agile development

  • Iterative, incremental development is consistent with the principle.
  • However, it easily drifts toward feature-first tactical implementation, and complexity tends to accumulate.
    • Recommended: grow the design by "increments of abstraction," not "increments of features." Abstractions that become necessary should be properly generalized (Ch. 6).

3) Unit tests

  • Making them tightly coupled with development improves quality and resilience to change.
  • When creating new code or modifying, update tests too to maintain coverage.
  • System (integration) tests are separately important as verification of integration close to production.

4) TDD (test-driven development)

  • The author is skeptical: tests written before code can unnecessarily constrain the design based on wrong assumptions.
  • However, the effect of making it a habit to "always write tests" is large.

5) Design patterns

  • They reuse proven design knowledge and also provide consistency.
  • However, overapplication backfires: have the courage not to use a pattern when something simpler is sufficient. Beware of tactical "showiness."

6) Aspect-oriented programming (AOP)

  • Aims to reduce duplication of cross-cutting concerns (logging/authorization, etc.) and improve modularity.
  • However, control flow becomes opaque, making it hard to understand and prone to bugs → not widely adopted in practice.

Practical checklist

  • Have I evaluated the new paradigm by "reduction of complexity"? (depth, hiding, change amplification, consistency)
  • For inheritance, start with interfaces, and prefer composition for sharing implementation.
  • The unit of iteration is abstractions, not features. When it becomes necessary, generalize and implement.
  • Build unit tests into the development flow (when modifying, modify tests too).
  • Choose patterns by fit to the problem ("patterns for the sake of patterns" is banned).
  • Always check whether cross-cutting concerns sacrifice visibility and readability.

Conclusion

  • Trends are means, not ends. Adopt and continue them only when they are a "lever for reducing complexity."
  • Even attractive new methods should be examined strategically from the standpoint of quality of abstraction, information hiding, and ease of change.

Chapter 20

  • Even in performance design, the most important principle is simplicity. Simple designs reduce complexity and often bring speedups too.
  • Premature optimization is injecting complexity. On the other hand, leaving it entirely alone risks "death by a thousand cuts," making the whole thing 5-10 times slower.
  • Develop a basic sense of costs and make naturally efficient design choices.

20.1 How to think about performance

  • Typical expensive operations
    • Network round trips (μs to ms), secondary storage I/O (μs to ms), dynamic memory allocation (allocation/free/GC overhead), cache misses (equivalent to hundreds of instructions).
  • How to learn: measure the cost of individual operations with microbenchmarks to build a feel for what is expensive and what is cheap.
  • Quick wins in design
    • If lookup by key is the main use, order is unnecessary → hash table (often 5-10 times faster than an ordered map).
    • For array elements, a flat array of structs rather than an array of pointers (better for allocation/locality).

20.2 Measure before and after modifying

  • Do not tinker by intuition. Measure first and identify hotspots.
  • Purposes: (1) identify the places with the greatest effect, (2) establish a baseline.
  • Re-measure after the change to confirm the effect. Revert changes that had little effect and only added complexity (unless they simplified things).

20.3 Design centered on the critical path

  • Imagine the ideal: code that completes the most frequent case with the minimum number of instructions. Temporarily ignore existing structure and special cases.
  • Look for a clean design as close as possible to the ideal (thin indirection for abstraction is acceptable if necessary).
  • Handle special cases off the path: the normal path has minimal branching (a single check at the start) → straight line afterward.

20.4 Example: RAMCloud's Buffer

  • Requirement: for variable-length data (such as KV), append and sequential read from the beginning in a few dozen instructions.
  • Solution: hold it as a linked list of segments.
    • append = link a new segment at the end (no need to copy/move existing data).
    • sequential read = scan the segments in order.
    • Removing from the front is done by removing whole segments or by managing an index of the consumed position (only a light check on the path).
    • Retrieving an arbitrary part is done by copying into a caller-side buffer (slow but off the path).
  • Result: the critical path is very fast, and the design is simple.

20.5 Summary (principles)

  1. Start simple: choose naturally efficient data structures and techniques.
  2. Measure → change → re-measure: follow measurements, not intuition.
  3. Minimize the critical path: cut branches and method crossings, and push special cases out.
  4. Even when major surgery is best: give priority to fundamental changes in algorithm/structure (e.g., introducing caching, bypassing the OS).

Practical checklist

  • Have I identified hotspots by measurement? (Not just top level but down to the breakdown)
  • Have I chosen cheaper equivalent means (hash vs. tree, flat array vs. pointer array)?
  • Have I reduced branches/calls/memory accesses on the critical path?
  • Have I branched special cases off the path with a single check at the first step?
  • Have I confirmed the effect numerically after the change and reverted optimizations that did not help?

Chapter 21: Decide What Matters Summary

  • The core of good design is separating "what is important" from "what is not important."
  • Emphasize and make visible what is important, and hide and minimize the impact of what is not important.
  • Many principles from earlier chapters, such as abstraction, naming, and performance design, are concrete applications of this idea.

21.1 How to decide what matters

  • Even when there are external constraints (e.g., performance), the designer must identify what is essential.
  • Identify the elements directly tied to satisfying the constraints and concentrate design resources there.

21.2 Minimize "important"

  • Reducing the number of items treated as important makes the system simpler.
    • Example: minimize the required constructor parameters and provide good defaults.
    • Example: absorb exceptions and settings at a low level / compute them automatically, reducing their exposure to higher levels.
  • Even when something is important, minimize the number of places it affects (information hiding).

21.3 How to emphasize what is important

  • Raise the priority of visibility: place the most important information at the top of a class/method.
  • Use appropriate naming to convey the essence intuitively.
  • State important invariants explicitly in the comment at the top of the class.
  • If the heart of a module is its interface, arrange it so that it is the first thing seen, clear, and minimal.

21.4 Common mistakes

  • Spending time on unimportant performance optimizations and complicating the design.
  • Details of the internal implementation (data structures, etc.) dominating the whole design.
    • Do not leak details unnecessary to users into the interface or structure.

21.5 Application beyond design

  • Testing: focus on the most important behavior and do not spend time on unimportant exhaustiveness.
  • Documentation: emphasize the key points for users and do not fill it with excessive detail.
  • Project management: concentrate resources on matters directly tied to success.
  • The skill of identifying "what matters" is trained through practice.

Practical checklist

  • Have I listed what is truly important in this change/feature?
  • Have I considered whether the number of items treated as important can be reduced further?
  • Have I placed important matters in the most visible places (API, top comments, naming)?
  • Are unimportant details sealed inside the implementation and not leaking upward?
  • Have I limited performance optimization to places that are truly important, based on measurement?

Summary

  • Design the system around what is important, and minimize the impact of and hide everything else.
  • This yields a design that is simple, easy to understand, and easy to maintain.

Conclusion

Complexity is the greatest enemy

  • The theme of this book is consistently "complexity."
  • Complexity makes software hard to build, slow, and hard to maintain.
  • Root causes: dependencies, obscurity, information leakage, unnecessary errors, and names that are too generic.

How to pursue simplicity

  • Design deep, general-purpose classes.
  • Define unnecessary errors out of existence.
  • Separate interface documentation from implementation documentation.
  • With an investment mindset, convert extra effort early on into future efficiency.

The wall of the initial investment

  • Extra work increases early in a project.
  • If you are not used to thinking about design, your pace slows.
  • For people who think "it's fine as long as it works," it feels boring and like a hindrance.

The enjoyment good design brings

  • Design is an intellectual game like a puzzle.
    • How to solve a problem with "the simplest structure."
    • Finding a simple and powerful solution is a pleasure.
  • Clear, simple design is beautiful.

Returns on investment

  • A module carefully designed early on → saves time through repeated reuse.
  • Clear documentation written half a year ago → helps when adding new features.
  • Acquiring design skills → you become able to produce good designs in a short time.
  • Once you have mastered it, good design can be done in about the same time as ad hoc design.

Good designers vs. bad designers

  • Good designers: can spend a lot of time in the design phase (= it is enjoyable).
  • Bad designers: spend most of their time tracking down bugs in complex, brittle code.

Final message

  • If you hone your design skills,
    • you can build high-quality software faster.
    • the development process itself becomes more enjoyable.
  • "Simplicity" is the greatest weapon, and it is beauty.