Cover of the summary of “Software Engineering at Google”

Book summary “Software Engineering at Google”: summary

Titus Winters

3,917 words · 22 min read

You face a constant struggle when software lives long enough to change hands, requirements shift, and original authors leave. Code degrades over time, and scaling an organization multiplies this complexity until manual processes fail completely.

Summary text

3,917 words · 22 min read

Introduction

You face a constant struggle when software lives long enough to change hands, requirements shift, and original authors leave. Code degrades over time, and scaling an organization multiplies this complexity until manual processes fail completely.

Software engineering is programming integrated over time, requiring scalable processes, automated testing, and a culture of humility, respect, and trust. Titus Winters, Tom Manshreck, and Hyrum Wright reveal how Google solves these sustainability challenges across massive codebases.

The book explores how engineering practices must evolve when scale forces you to treat code as a long-term asset. You will discover answers to several core questions:

  • How do you maintain code health across decades and thousands of developers?
  • What organizational culture enables rapid, safe innovation?
  • Why do traditional testing and building methods fail at Google's scale?

1. Software Engineering Versus Programming

Programming is the immediate act of generating new code. Software engineering integrates programming over time, dealing with modifications, maintenance, and long-term evolution across changing environments.

Software engineering differs from programming because it must account for the passage of time and ongoing maintenance.

Short-lived code and long-lived code differ in expected life span by a factor of roughly 100,000. Sustainability is the capability of a project to react to valuable changes in technology or business requirements throughout its expected life span.

With a sufficient number of users, every observable behavior of a system will eventually be depended upon, regardless of published contracts. Hyrum's Law acts as the software equivalent of entropy.

Programmers relying on hash iteration order as an inefficient random-number generator make it impossible to change hash algorithms without breaking downstream users.

The popular idea of the lone programming genius is a myth born out of personal insecurity and the fear of being judged. Programmers often hide their work in progress to protect their egos, fearing peers will see mistakes and realize they are not geniuses.

In 2006, Google launched Project Hosting, and by mid-2008, developers repeatedly requested features to hide specific branches or wipe code history out of fear of peer judgment. Working entirely in isolation drastically increases the risk of building the wrong thing and severely limits professional growth.

A bicycle-design enthusiast spent months building a gear shifter prototype in secret, only to discover a neighbor already built a similar device much faster by collaborating with a local bike shop.

Healthy engineering collaboration rests upon three essential social pillars: humility, respect, and trust.

  • Pillar 1: Humility - Understand you are not the center of the universe, you are neither omniscient nor infallible, and you are open to self-improvement.
  • Pillar 2: Respect - Genuinely care about others you work with, treat them kindly, and appreciate their abilities and accomplishments.
  • Pillar 3: Trust - Believe others are competent and will do the right thing, and be OK with letting them drive when appropriate.

2. Team Culture and Leadership

Successful software organizations rely on psychological safety, servant leadership, and social pillars of humility, respect, and trust.

You can maintain these pillars in daily collaboration through concrete behaviors:

  • Pillar 1: Humility - Understand you are not the center of the universe, you are neither omniscient nor infallible, and you are open to self-improvement.
  • Pillar 2: Respect - Genuinely care about others you work with, treat them kindly, and appreciate their abilities and accomplishments.
  • Pillar 3: Trust - Believe others are competent and will do the right thing, and be OK with letting them drive when appropriate.

Psychological safety forms the critical foundation for learning and sharing knowledge. Without it, you face information islands, single points of failure, and fear.

Jerry, a new engineer, apologized for leaving fifteen minutes early for an appointment, expecting a strict desk-presence rule. His manager simply told him that as long as the work gets done, the exact schedule does not matter, treating him like an adult.

A servant leader acts like a majordomo who removes obstacles and protects the health of the team.

You must handle low performance promptly because hope is not a strategy. When addressing poor performance, you should follow specific steps:

  • Set up a specific time frame such as two months for improvement.
  • Establish small, incremental, and measurable goals.
  • Meet with the team member every week to check on progress.
  • Set explicit expectations around each upcoming milestone.

You can drive broad changes through influence without formal authority. Jeff Dean led only a fraction of Google engineering, yet his technical direction influenced the entire organization. Separately, the Data Liberation Front team of less than half a dozen engineers got more than 50 Google products to export data via Google Takeout by building a simple integration tool.

3. Scaling Organizations and Productivity

Managing scale requires structured knowledge sharing, measurable productivity frameworks, and consistent style rules enforced by tooling.

Codebases should prioritize the reader over the author because code is read far more frequently than it is written. You should value code that is simple to read over code that is simple to write, even if it requires more upfront effort or typing descriptive names. Style rules mandate explicit in-place evidence of design intent to enable local reasoning without inspecting distant implementations.

Using std::unique_ptr along with explicit std::move semantics in C++ rather than raw pointers makes pointer ownership transfer completely clear at the call site.

Automated tooling is essential for enforcing rules reliably, scaling efficiently, and eliminating human bias. Depending purely on human memory or documentation leads to inconsistent application. Automated static analysis and formatting tools apply a single, unchanging definition of rules across the entire codebase at a constant cost regardless of organizational growth.

Tools like clang-tidy for C++, Error Prone for Java, and formatters like clang-format and gofmt handle static analysis and code formatting automatically. A mid-2018 informal survey by C++ librarians found that 90% of C++ style rules could be automatically verified.

The Goals, Signals, Metrics framework prevents the streetlight effect by establishing high-level goals and unmeasurable signals before deciding on proxy metrics. The GSM framework forces teams to define a desired end result, determine how to know if it is achieved, and finally find a measurable proxy. This principled approach prevents the streetlight effect and avoids metrics creep or bias.

To apply this framework successfully, you can follow these steps:

  • Define a Goal as a desired end result phrased at a high level without specific measurement references.
  • Identify a Signal as how you might know that you have achieved the end result, recognizing it may not be directly measurable.
  • Determine a Metric as a measurable proxy for the signal.

Measurement is expensive and should only be undertaken if the results will lead to actionable business decisions. You must triage your measurements before investing time and infrastructure.

The release tool team at Google requested measurements for a workflow system change, but admitted they would implement the change anyway because it was performant and lowered maintenance burdens, making the productivity measurement a vanity metric.

4. Code Review and Documentation

Rigorous code reviews and code-like documentation practices maintain codebase health, consistency, and long-term historical records.

Every change must be approved before being committed. The process relies on an LGTM bit for correctness and comprehension, owner approval for directory boundaries, and readability approval for language style and consistency.

  • A user writes a change in their workspace and uploads a snapshot patch and description to the code review tool.
  • The author applies automated comments or self-review, then mails the change to reviewers.
  • Reviewers open the change and post comments on the diff.
  • The author modifies the change, uploads new snapshots, and replies to feedback.
  • Reviewers agree to the change and mark it as looks good to me (LGTM).
  • The author resolves all comments and commits the change to the codebase.

Keeping code changes small is the most important practice for maintaining a nimble review process and enabling rapid feedback. Limit changes to about 200 lines of code to keep reviews fast and manageable. Provide initial feedback within 24 working hours. Focus the change on a single issue rather than bundling unrelated modifications. About 35 percent of changes at Google affect a single file.

Documentation must be treated like code and integrated into the traditional developer workflow to ensure quality and scalability.

Google transitioned from a flat-namespace internal wiki called GooWiki, which suffered from unowned and obsolete pages, to source-controlled markdown files and g3doc embedded alongside source code. When it was deprecated, 90% of documents on GooWiki had no views or updates in the previous few months.

  • Have internal policies or rules to be followed
  • Be placed under source control
  • Have clear ownership responsible for maintaining the docs
  • Undergo reviews for changes (and change with the code it documents)
  • Have issues tracked, as bugs are tracked in code
  • Be periodically evaluated (tested, in some respect)
  • If possible, be measured for aspects such as accuracy, freshness, etc.

Reference documentation embedded in code files should follow strict structural and grammatical conventions to maximize readability. C++, Java, Python, and Go codebases rely heavily on code comments placed directly in header files or source files next to the implementation. Google's StrCat() and StrAppend() header files use structured file comments, and the AlphaNum class uses noun-based descriptions.

5. Testing Overview and Unit Testing

Automated testing is not just about catching bugs; its primary motivation is to support the ability to change software safely and quickly.

Automated tests catch mistakes rapidly, enabling engineers to add new features, refactor code, and redesign systems with confidence. Without robust testing, scaling a codebase leads to massive productivity drops and buggier releases as teams grow.

In 2005, the Google Web Server had no automated testing policy, and over 80 percent of production pushes contained user-affecting bugs that required rollbacks.

Google classifies tests by size rather than traditional unit or integration terms, focusing strictly on resource consumption, speed, and determinism. Test size determines what resources a test is allowed to consume. Small tests run in a single process without I/O or sleeping, medium tests can span multiple processes within a single machine, and large tests can span multiple machines.

To maintain a healthy codebase, you should follow specific testing practices:

  • Write small tests that run in a single process without blocking calls, network access, or disk access
  • Write medium tests that can use multiple processes and blocking calls restricted to localhost
  • Write large tests that allow multi-machine and remote cluster interactions for full system validation
  • Target an approximate mix of 80% narrow-scoped unit tests, 15% medium-scoped integration tests, and 5% end-to-end tests
  • Avoid the ice cream cone and hourglass antipatterns by maintaining a solid unit test base
  • Write an automated test for every system behavior, performance metric, accessibility rule, or security feature you want to preserve
  • Measure code coverage only from small tests to avoid inflation from larger tests
  • Focus on whether customer-expected behaviors are verified rather than chasing a coverage number

To prevent brittle tests that break during internal refactorings, tests should invoke the system under test exclusively through its public API rather than accessing implementation details.

Peering into internal state or testing private methods causes tests to break whenever internal code is renamed or restructured, even if the change is entirely invisible to real users. Testing against public APIs aligns tests with real user behavior.

Mary spends hours updating tests that break because automated tests relied on internal code assumptions rather than public methods.

Test code should prioritize DAMP over strict adherence to the DRY principle. While DRY reduces code duplication in production, applying it excessively to tests hides critical details inside helper methods and makes tests unclear. A small amount of duplication is acceptable in tests if it keeps each test complete, concise, and understandable entirely within its own body.

Unit tests should be structured around behaviors rather than mirroring the structure of production methods. Mapping every production method to a single test method leads to convoluted tests as methods grow in complexity. Instead, you should write distinct tests for each distinct behavior, commonly expressed through given, when, and then components.

A single method-driven test checking both transaction success and low-balance warnings grows bloated, whereas splitting them into behavior-driven tests improves clarity and maintainability.

Instead of relying on rigid shared constants across a test suite, you should use helper methods with reasonable defaults or builder patterns to construct test data. Shared constants make it hard to understand why specific values were chosen and encourage inappropriate reuse. Helper methods or builders allow individual tests to specify only the parameters they care about while keeping data generation clean.

Using the Builder pattern in Java allows tests to instantiate contact objects with predefined defaults while overriding only the specific fields needed for a given test case.

You can apply these unit testing practices directly:

  • If a method or class exists only to support one or two other classes, it should be tested through those classes instead of directly.
  • If a package or class is designed to be accessible by anyone, it constitutes a unit that should be tested directly via its public API.
  • If a package or class is a support library accessible only by its owners, it should be considered a unit and tested directly.
  • Define the setup of the system using a given component.
  • Define the action taken on the system using a when component.
  • Validate the result using a then component.
  • Create a helper method wrapping a constructor by defining arbitrary default values for each parameter.
  • Call the helper method in tests, specifying values only for parameters relevant to the test case.
  • For languages without named parameters, return a mutable builder object and call build() to retrieve the constructed value.

6. Test Doubles and Larger Testing

Fakes and real implementations provide better fidelity than excessive stubbing, supported by larger integration and regression tests.

You choose between three primary test double techniques: faking, stubbing, and interaction testing. Classical testing prefers real implementations over test doubles to ensure high fidelity and confidence. You avoid mockist testing because it pollutes tests with repetitive setup that drifts from reality.

Faking provides a lightweight alternative implementation like an in-memory database. You use fakes when real implementations are infeasible, maintaining high fidelity to API contracts while executing quickly. You follow specific practices for fakes:

  • Write fakes primarily at the root of code that is not feasible for use in tests.
  • Ensure the fake maintains fidelity to the API contracts of the real implementation.
  • Have the fake fail fast if an unsupported code path is executed.
  • Write dedicated tests for the fake or run contract tests against both the real implementation and the fake.

You prefer state testing over interaction testing because interaction testing exposes implementation details and leads to brittle change-detector tests. You follow these interaction testing guidelines:

  • Prefer state testing over interaction testing.
  • Perform interaction testing only for state-changing functions rather than non-state-changing functions.
  • Avoid overspecifying which functions and arguments are validated in interaction tests.

A/B diff regression testing and production chaos engineering validate live system resilience and catch unintended behavioral changes.

Larger tests follow a common workflow consisting of four distinct phases: obtaining a system under test, seeding necessary test data, performing actions, and verifying behaviors. You manage test data using specific strategies:

  • Handcraft test data manually for multiple services in a large SUT.
  • Copy production data to provide a realistic baseline.
  • Sample data to reduce volume, testing time, and complexity.

A/B diff regression testing is one of the most common forms of larger testing at Google, comparing behavior between candidate and base versions of public APIs. Google has been running tests based on the A/B diff model since 2001 for products like Ads, Search, and Maps. You verify system behavior through manual inspection, explicit assertions, or automated A/B comparisons. You follow these verification approaches:

  • Perform manual regression testing or exploratory interactions using a consistent test plan.
  • Write explicit assertions to check intended system behavior.
  • Run two copies of the SUT with the same data and compare outputs via A/B comparison.

You integrate larger tests into the developer workflow by balancing test speed, reducing flakiness, and providing clear error messages. You replace timed sleeps and address flakiness with specific practices:

  • Poll for state transitions over a time window with high frequency instead of using timed sleeps.
  • Implement event handlers or subscribe to notification systems for event completion.
  • Provide context-rich failure messages that explain expected versus actual outcomes.
  • Establish documented ownership for every larger test using regular code owners or structured annotations.

Record/replay proxies help validate external service dependencies by recording traffic during a larger post-submit test and replaying it during smaller developer-workflow tests. You configure these proxies using specific workflows:

  • Run the larger test in Record Mode to generate and update traffic logs.
  • Run the smaller test in Replay Mode during development and presubmit testing using matchers.

Production-based testing methods such as probers, canary analysis, and chaos engineering validate live system resilience by injecting faults or monitoring real-world health metrics. You deploy these methods in production environments:

  • Deploy probers to run encoded assertions against production environments.
  • Execute canary analysis during staged production releases.
  • Inject infrastructure faults continuously using chaos engineering tools.

Google runs an annual war game called DiRT (Disaster Recovery Testing) simulating disasters like an earthquake isolating the Mountain View headquarters, and uses an internal system called Catzilla to run thousands of chaos tests each week by Google teams using Catzilla.

7. Deprecation and Version Control

Large codebases require a single source of truth, the One-Version Rule, and structured deprecation processes to manage liabilities.

Deprecation processes fall on a continuum from advisory approaches lacking deadlines to compulsory approaches backed by enforcement. Advisory deprecations act as low-priority suggestions or announcements of new systems. Compulsory deprecations establish a deadline, are actively staffed by a specialized team, and feature an enforcement mechanism.

Scalable deprecation relies on tooling categorized into discovery, migration, and backsliding prevention. Code Search and runtime logging identify who uses an obsolete system. Migration tools automate codebase updates, and static analysis frameworks stop developers from introducing new dependencies.

You should design systems with their eventual decommissioning in mind, much like complex physical infrastructure. Nuclear power stations incorporate eventual decommissioning into their design and allocate funds for this purpose, as outlined in IAEA Technical Reports Series No. 382.

When you design for deprecation, you must answer specific questions:

  • How easy will it be for my consumers to migrate from my product to a potential replacement?
  • How can I replace parts of my system incrementally?

Google scales its development by housing the vast majority of its source code in a single massive centralized repository. Piper runs as a distributed microservice handling 60,000 to 70,000 commits per workday across 50,000 engineers while keeping common operations fast.

Organizations should prefer source control problems over dependency management problems because managing external dependencies without full visibility introduces immense complexity.

The One-Version Rule dictates that developers must have no choice regarding which version of a dependency to use. Allowing multiple versions or forked libraries on trunk creates partitioning and diamond dependency problems that lead to build failures. Enforcing a single version eliminates this entire class of costly incompatibilities.

Before you decide to import a third-party dependency, you must evaluate specific external and internal questions:

  • Does the project have tests that you can run?
  • Do those tests pass?
  • Who is providing that dependency?
  • What sort of compatibility is the project aspiring to?
  • Does the project detail what sort of usage is expected to be supported?
  • How popular is the project?
  • How long will we be depending on this project?
  • How often does the project make breaking changes?
  • How complicated would it be to implement that functionality within your organization?
  • What incentives will we have to keep this dependency up to date?
  • Who will perform an upgrade?
  • How difficult do we expect it to be to perform an upgrade?

8. Build Systems, CI, and Continuous Delivery

Artifact-based build systems, continuous integration pipelines, and small batch releases drive speed, reliability, and safety.

Task-based tools like Ant, Maven, and Gradle execute arbitrary scripts, forcing the system to rerun tasks conservatively because it cannot verify their side effects. Artifact-based systems like Blaze, Bazel, Pants, and Buck act like functional programming functions, treating targets as immutable transformations of inputs to outputs. Google utilizes ObjFS as a distributed remote cache featuring a FUSE daemon named objfsd for on-demand file loading, and Forge as its remote execution system managed by a Scheduler and executed across an Executor pool.

Google runs millions of builds executing millions of test cases and producing petabytes of build outputs from billions of lines of source code every day.

To protect engineer productivity, presubmit testing must be limited to fast, reliable checks like unit tests, while larger and slower test suites run asynchronously on post-submit. Google's global continuous build tool TAP handles more than 50,000 unique changes and runs over four billion individual test cases daily, achieving an average change submission wait time of around 11 minutes.

Releasing software frequently in small batches results in higher quality and lower risk because smaller changes make troubleshooting trivial.

  • Release frequently and in small batches.
  • Reduce or remove repetitive overhead of frequent releases.
  • Strive for modular architecture to isolate changes and make troubleshooting easier.
  • Measure key health indicators like crashes or latency and keep improving them.
  • Use A/B testing on health metrics to ensure quality.
  • Roll out changes to a few users before shipping to everyone.

Containerization provides lightweight isolation and bin-packing, solving the inefficiency and resource interference of a one-to-one mapping between machines and programs. In 2011, engineers working on Borg discovered that process ID space exhaustion set to a default limit of 32,000 PIDs caused an isolation failure, requiring limits on thread and process creation.

Conclusion

Sustainable software engineering requires you to view code as a living artifact that must survive time, organizational growth, and continuous change.

The ideas fit together as an integrated ecosystem where culture, processes, and tools reinforce one another. You cannot achieve sustainable speed without automated testing, clear documentation, psychological safety, and strict code health standards working in unison.

To apply these principles in your daily work, follow these practical rules:

  1. Prioritize sustainability over short-term convenience by treating time as the primary constraint on your codebase.
  2. Cultivate humility, respect, and trust within your team to build psychological safety and constructive code reviews.
  3. Enforce consistency through automated tooling and style guides rather than manual oversight.
  4. Invest in comprehensive automated testing that spans unit, integration, and large-scale regression checks.
  5. Manage dependencies and deprecations proactively to keep your codebase lean and maintainable.

10 Key Ideas

  1. Software engineering differs from programming because it must account for the passage of time and ongoing maintenance.
  2. Successful software organizations rely on psychological safety, servant leadership, and social pillars of humility, respect, and trust.
  3. Managing scale requires structured knowledge sharing, measurable productivity frameworks, and consistent style rules enforced by tooling.
  4. Rigorous code reviews and code-like documentation practices maintain codebase health, consistency, and long-term historical records.
  5. Automated testing is not just about catching bugs; its primary motivation is to support the ability to change software safely and quickly.
  6. Fakes and real implementations provide better fidelity than excessive stubbing, supported by larger integration and regression tests.
  7. Large codebases require a single source of truth, the One-Version Rule, and structured deprecation processes to manage liabilities.
  8. Artifact-based build systems, continuous integration pipelines, and small batch releases drive speed, reliability, and safety.
  9. With a sufficient number of users, every observable behavior of a system will eventually be depended upon, regardless of published contracts.
  10. Sustainable software engineering requires you to view code as a living artifact that must survive time, organizational growth, and continuous change.

A summary is an independent retelling, not the text of the book.

Cover of the summary of “The Last Whalers” 20 min read

The Last Whalers

Doug Bock Clark
Cover of the summary of “Confess” 21 min read

Confess

Rob Halford
Cover of the summary of “Toast” 17 min read

Toast

Nigel Slater
Cover of the summary of “Ace Voices” 24 min read

Ace Voices

Eris Young
Cover of the summary of “For Small Creatures Such as We” 17 min read
Cover of the summary of “Two Trees Make a Forest” 16 min read
Cover of the selected audiobook Choose a chapter The player is ready
0:00 0:00

Volume