Searching...
English
EnglishEnglish
EspañolSpanish
简体中文Chinese
FrançaisFrench
DeutschGerman
日本語Japanese
PortuguêsPortuguese
ItalianoItalian
한국어Korean
РусскийRussian
NederlandsDutch
العربيةArabic
PolskiPolish
हिन्दीHindi
Tiếng ViệtVietnamese
SvenskaSwedish
ΕλληνικάGreek
TürkçeTurkish
ไทยThai
ČeštinaCzech
RomânăRomanian
MagyarHungarian
УкраїнськаUkrainian
Bahasa IndonesiaIndonesian
DanskDanish
SuomiFinnish
БългарскиBulgarian
עבריתHebrew
NorskNorwegian
HrvatskiCroatian
CatalàCatalan
SlovenčinaSlovak
LietuviųLithuanian
SlovenščinaSlovenian
СрпскиSerbian
EestiEstonian
LatviešuLatvian
فارسیPersian
മലയാളംMalayalam
தமிழ்Tamil
اردوUrdu
Code That Fits in Your Head

Code That Fits in Your Head

Heuristics for Software Engineering
by Mark Seemann 2021 406 pages
4.11
283 ratings
Listen
Try Full Access for 7 Days
Unlock listening & more!
Continue

Key Takeaways

1. Software Development is Engineering, Not Just Art

Software engineering should make the software development process more regular. It should sustain its organisation.

Beyond metaphors. Software development has often been likened to building a house or growing a garden, but these metaphors fall short. Building implies a finished state, while successful software endures and evolves. Gardening emphasizes tending, but doesn't explain creation. True software engineering aims for a repeatable, sustainable process that supports the long-term goals of the organization.

Heuristics and process. Moving towards engineering involves adopting practical heuristics and processes. These aren't rigid laws but guidelines that improve outcomes without necessarily increasing individual skill. Simple steps like using version control, automating builds, and turning on compiler warnings are foundational engineering practices that prevent decay and increase efficiency.

Sustainable pace. The goal is not speed for its own sake, but a sustainable pace of development. This means being able to add features, fix bugs, and make changes consistently over months and years. Engineering practices, like those borrowed from other disciplines (e.g., checklists from aviation), help achieve this by managing complexity and reducing errors.

2. Tackle Complexity to Fit Code in Your Head

The human brain can deal with limited complexity.

Cognitive limits. Programming is difficult because software is intangible and complex, far exceeding the capacity of human short-term memory (roughly seven items). When code is too complex, our brains struggle to understand it, leading to mistakes and slow development. We must actively manage complexity to make code comprehensible.

What you see. Our brains tend to jump to conclusions based on immediately available information ("what you see is all there is"). Code with hidden dependencies, global variables, or implicit side effects is hard to reason about because the relevant information isn't visible, leading to incorrect inferences.

Design for the brain. Software engineering must address these cognitive constraints. This means structuring code into small, self-contained chunks that fit within our mental capacity. Metrics like cyclomatic complexity or counting variables can serve as useful, albeit imperfect, guides to identify areas that exceed humane limits.

3. Prioritize Readability: Code is Read More Than Written

You spend more time reading code than writing it.

Optimize for readers. A fundamental truth of software development is that code is read far more often than it is written. Every minute invested in making code easier to understand pays dividends over its lifetime. Therefore, optimizing code for readability should be a primary goal.

Context is lost. When you write code, you have full context. When you read it later, or someone else reads it, that context is gone. The code itself must serve as the primary source of truth and understanding. Relying on external documentation or comments is risky, as they can become outdated.

Beyond intuition. Writing readable code isn't always intuitive; our brains are prone to errors (like the bat-and-ball puzzle). We need actionable heuristics and practices to guide us. This includes favoring clear structure, meaningful names, and explicit relationships over clever shortcuts or implicit behaviors.

4. Use Automated Drivers for Change

Using drivers of change gives rise to a whole family of x-driven software development methodologies.

External guidance. Relying solely on intuition when writing code is error-prone. Using external "drivers" for change, such as automated tests or static analysis tools, provides a form of double-entry bookkeeping that helps ensure correctness and guides development.

Test-driven development. Test-driven development (TDD) is a prime example, following a Red/Green/Refactor cycle:

  • Red: Write a failing test (hypothesis).
  • Green: Write minimal code to pass the test (experiment).
  • Refactor: Improve the code while keeping tests green (verification).
    This process provides rapid feedback and ensures that tests accurately reflect desired behavior.

Beyond tests. Drivers aren't limited to TDD. Static analysis tools, linters, and compiler warnings (treated as errors) also act as drivers, prompting code changes based on predefined rules and best practices. Using multiple drivers simultaneously can lead to more robust and maintainable code.

5. Build Quality In Through Encapsulation and Validation

The most important notion is that an object should guarantee that it’ll never be in an invalid state.

Protecting invariants. Encapsulation is not just about hiding data behind getters and setters; it's about ensuring an object is always in a valid state. Objects should protect their invariants, guaranteeing that interactions adhere to a contract of preconditions and postconditions.

Design by contract. Explicitly designing with contracts in mind clarifies valid inputs and guaranteed outputs. Guard clauses enforce preconditions, failing fast when input is invalid. This shifts the responsibility for validity from the caller to the object itself, reducing defensive coding elsewhere.

Poka-yoke design. Design APIs to be difficult to misuse, making illegal states unrepresentable. Leveraging the type system (e.g., nullable reference types, custom value types) can prevent errors at compile time rather than runtime, providing faster feedback and increasing confidence.

6. Decompose and Compose for Understandable Architecture

At every zoom level, the complexity of the code remains within humane bounds.

Preventing code rot. Code bases naturally tend towards increased complexity and decay if not actively managed. Methods grow, logic intertwines, and the code becomes harder to understand and change. Establishing thresholds for complexity metrics (like cyclomatic complexity or lines of code) can signal when decomposition is necessary.

Fractal architecture. Aim for an architecture where code is understandable at any level of detail. High-level components should be composed of smaller, well-defined parts, and zooming into those parts should reveal a similar structure of manageable complexity. This self-similarity, like mathematical fractals, helps the code fit in your brain.

Functional core. Favor sequential composition of pure functions (Queries without side effects) over nested composition of procedures (Commands with side effects). Pure functions are referentially transparent, meaning a function call can be replaced by its result, simplifying reasoning. Push impure actions to the edges of the system, creating a functional core and imperative shell.

7. Foster Collective Code Ownership and Collaboration

Does the team contain more than one person comfortable working with a particular part of the code?

Increase the bus factor. Relying on single individuals to "own" parts of the code base creates critical dependencies and hinders flexibility. Collective code ownership, where multiple team members are comfortable working with any part of the code, increases the team's resilience and facilitates refactoring.

Real-time collaboration. Practices like pair programming and mob programming foster collective ownership by ensuring multiple eyes see every line of code as it's written. This provides continuous, low-latency review and knowledge transfer, although it may not suit all individuals or situations.

Structured reviews. Formal code reviews, often facilitated by tools like pull requests, are a proven method for finding defects and promoting shared understanding. To be effective, reviews must be timely (low latency), focused (small changes), and involve genuine feedback, with rejection being a viable option for problematic changes.

8. Augment and Refactor Incrementally with Patterns

For any significant change, don’t make it in-place; make it side-by-side.

Continuous integration. Integrating code frequently (e.g., every few hours) is crucial to avoid merge conflicts and maintain a healthy code base. When implementing large features that take longer than an integration cycle, hide incomplete functionality behind feature flags to enable frequent merging without exposing unfinished work.

The Strangler pattern. When making significant changes or refactoring existing components, avoid modifying code in place if it's complex or widely used. Instead, apply the Strangler pattern:

  • Add the new functionality or component alongside the old.
  • Gradually migrate callers from the old to the new.
  • Once all callers are migrated, delete the old code.
    This allows for incremental, low-risk changes that can be integrated and deployed continuously.

Versioning changes. When modifying public APIs, be mindful of breaking changes. Semantic Versioning provides a clear convention for communicating the impact of changes. If a breaking change is necessary, consider deprecating the old API first to warn consumers before removing it in a new major version.

9. Troubleshoot Systematically to Understand Problems

Try to understand what’s going on.

Scientific approach. When encountering defects or unexpected behavior, resist the urge to randomly try solutions. Instead, adopt a systematic approach akin to the scientific method:

  • Formulate a hypothesis about the cause.
  • Design and perform an experiment (e.g., write a test, simplify the code).
  • Compare the outcome to your prediction.
    Repeat this cycle until you understand the root cause, not just the symptom.

Reproduce as tests. The most effective way to understand and prevent regressions for defects is to reproduce them as automated tests. A test that reliably fails when the bug is present validates your understanding and serves as a permanent guard against the issue returning.

Bisection technique. For elusive bugs, especially those introduced recently, use bisection. This involves repeatedly dividing the relevant code or commit history in half and checking which half contains the problem. Git's bisect command automates this process for finding the specific commit that introduced a bug.

10. Establish a Sustainable Personal and Team Rhythm

Being away from the computer is remarkably productive.

Time-boxed work. Structure your personal work with time-boxing (e.g., 25 minutes of focus, 5-minute break). This helps manage daunting tasks, maintains focus, and provides regular opportunities to step back and gain perspective, potentially avoiding wasted effort.

Breaks and reflection. Deliberately taking breaks away from the computer is crucial for intellectual work. Physical activity or changing your environment can stimulate subconscious processing and lead to breakthroughs. Don't confuse being "in the zone" with guaranteed productivity; regular breaks allow for critical evaluation.

Scheduled maintenance. Teams should establish rhythms for activities that are easily forgotten but critical for long-term health. This includes:

  • Regularly updating dependencies to avoid accumulating technical debt.
  • Scheduling proactive maintenance like checking backups or renewing certificates.
  • Being mindful of Conway's Law, ensuring team communication structure supports desired code architecture.

11. Address Cross-Cutting Concerns with Decorators

Push all of that to the edge of the system; your Main method, your Controllers, your message handlers, etcetera.

Separating concerns. Concerns like logging, security, caching, and monitoring often apply across many parts of a system. Mixing these cross-cutting concerns directly into core business logic increases complexity and makes code harder to change.

Decorator pattern. The Decorator design pattern is an excellent way to handle cross-cutting concerns. It allows you to wrap existing objects with new behavior without modifying the original object's code. This keeps concerns separate and makes them easier to manage and compose.

Logging strategy. A key cross-cutting concern is logging. Aim for "Goldilogs" – logging just enough information to reproduce execution. Log all impure actions (non-deterministic operations, side effects) but not the results of pure functions, as those can be reproduced deterministically if the inputs are known.

12. Measure and Analyze Code Behavior

If you think it’s important, measure.

Beyond intuition. While intuition and experience are valuable, objective measurements can provide insights into code quality and development processes that are otherwise invisible. Metrics should serve as guides, not rigid laws, prompting investigation rather than dictating solutions.

Complexity metrics. Simple metrics like cyclomatic complexity and lines of code can indicate areas of potential complexity that may exceed human cognitive limits. Monitoring these metrics and setting thresholds can help prevent code rot.

Behavioral analysis. Analyzing version control history can reveal patterns invisible in static code. Tools can identify:

  • Hotspots: Files that change frequently and are complex.
  • Change coupling: Files that tend to change together, indicating hidden dependencies.
  • Knowledge distribution: Which developers work on which parts of the code, revealing potential knowledge silos or bus factor risks.

Last updated:

Review Summary

4.11 out of 5
Average of 283 ratings from Goodreads and Amazon.

Code That Fits in Your Head receives mostly positive reviews, praised for its practical advice, clear writing style, and comprehensive coverage of software development best practices. Readers appreciate the book's focus on writing maintainable, efficient code and its accessibility to developers at various experience levels. Some criticisms include repetition of known concepts and occasional lack of depth. Many reviewers recommend it as a valuable resource for improving coding skills, particularly for intermediate developers. The book's emphasis on cognitive limitations in programming and its extensive references to other literature are highlighted as strengths.

Your rating:
Be the first to rate!

About the Author

Mark Seemann is a respected software developer and author known for his expertise in clean code, software architecture, and functional programming. He has written multiple books on software development, including the highly regarded "Dependency Injection in .NET." Seemann is recognized for his ability to explain complex concepts clearly and practically. He maintains a popular blog where he shares insights on various programming topics, particularly focusing on functional programming. Seemann's work often emphasizes the importance of writing maintainable, efficient code and adopting best practices in software development. His contributions to the field have earned him a reputation as a thought leader in the software engineering community.

Download EPUB

To read this Code That Fits in Your Head summary on your e-reader device or app, download the free EPUB. The .epub digital book format is ideal for reading ebooks on phones, tablets, and e-readers.
Download EPUB
File size: 2.94 MB     Pages: 13
Listen
Now playing
Code That Fits in Your Head
0:00
-0:00
Now playing
Code That Fits in Your Head
0:00
-0:00
1x
Voice
Speed
Dan
Andrew
Michelle
Lauren
1.0×
+
200 words per minute
Queue
Home
Swipe
Library
Get App
Create a free account to unlock:
Recommendations: Personalized for you
Requests: Request new book summaries
Bookmarks: Save your favorite books
History: Revisit books later
Ratings: Rate books & see your ratings
200,000+ readers
Try Full Access for 7 Days
Listen, bookmark, and more
Compare Features Free Pro
📖 Read Summaries
All summaries are free to read in 40 languages
🎧 Listen to Summaries
Listen to unlimited summaries in 40 languages
❤️ Unlimited Bookmarks
Free users are limited to 4
📜 Unlimited History
Free users are limited to 4
📥 Unlimited Downloads
Free users are limited to 1
Risk-Free Timeline
Today: Get Instant Access
Listen to full summaries of 73,530 books. That's 12,000+ hours of audio!
Day 4: Trial Reminder
We'll send you a notification that your trial is ending soon.
Day 7: Your subscription begins
You'll be charged on Jul 23,
cancel anytime before.
Consume 2.8x More Books
2.8x more books Listening Reading
Our users love us
200,000+ readers
"...I can 10x the number of books I can read..."
"...exceptionally accurate, engaging, and beautifully presented..."
"...better than any amazon review when I'm making a book-buying decision..."
Save 62%
Yearly
$119.88 $44.99/year
$3.75/mo
Monthly
$9.99/mo
Start a 7-Day Free Trial
7 days free, then $44.99/year. Cancel anytime.
Scanner
Find a barcode to scan

Settings
General
Widget
Loading...