Chapter 12: Emergence

clean-code emergence simple-design dry refactoring design-patterns

Status: Notes complete
Difficulty: Medium
Time to complete: ~45 min read


Overview

“Emergence” is the idea that a good overall design does not have to be planned in full detail upfront — it can emerge from following a small set of disciplined practices consistently. Kent Beck articulated this as four rules of simple design. If you follow these four rules, your code will tend toward good architecture naturally, even when you don’t consciously plan it.

The rules are stated in priority order — when they conflict, the earlier rule wins:

  1. Runs all the tests — the system must be verifiable
  2. No duplication — every piece of knowledge has one representation
  3. Expresses the intent of the programmer — code communicates clearly
  4. Minimizes the number of classes and methods — avoid unnecessary abstractions

This chapter is about how these four rules interact, and how following them creates emergent simplicity over time.


The Problem: What Bad Code Looks Like

Code that violates these four rules tends to look like this:

// BAD — violates all four rules at once
public class ReportUtils {
    public static String generate1(Employee emp, int month) {
        // Copied from somewhere else; no tests cover this
        double total = 0;
        for (int i = 0; i < emp.getSales().size(); i++) {
            if (emp.getSales().get(i).getMonth() == month) {
                total += emp.getSales().get(i).getAmt();
            }
        }
        String out = "Report for: " + emp.fn + " " + emp.ln + "\n";
        out += "Month: " + month + "\n";
        out += "Total: " + total + "\n";
        return out;
    }
 
    public static String generate2(Employee emp, int month) {
        // Near-duplicate of generate1 with a minor format change — no abstraction
        double t = 0;
        for (Sale s : emp.getSales()) {
            if (s.getMonth() == month) t += s.getAmt();
        }
        return String.format("%-20s Month %-2d  $%.2f%n",
            emp.fn + " " + emp.ln, month, t);
    }
}

What’s wrong:

  • No tests — we cannot verify correct behavior or safely change this
  • Duplicated logic — the sale-summing loop is copy-pasted with minor variation
  • Expresses nothing — generate1, generate2, fn, ln, t, out all hide intent
  • Over-proliferates methods — two methods doing nearly the same thing when one would do

Core Principles

1. Rule 1: Runs All the Tests

Why this rule exists: A design that cannot be verified is worthless. If you cannot test a system, you have no way to know if it works — or whether a change broke it. Rule 1 is the foundation: it is not possible to follow the other three rules meaningfully if the system has no test suite.

The deeper insight: writing tests creates pressure toward better design. A class that is hard to test is almost always poorly designed. Tests push you toward:

  • Small, focused classes (easier to test in isolation)
  • Dependency injection (so fakes can be substituted)
  • Low coupling (so one class doesn’t drag in its entire dependency tree in tests)
  • Single Responsibility (a class that does one thing is straightforward to test)
// BAD — untestable; EmailService is constructed inside SalesReport; no seam for a fake
public class SalesReport {
    public String generate(Employee emp, Month month) {
        EmailService emailer = new SmtpEmailService("smtp.corp.com", 587); // hardcoded!
        double total = sumSales(emp, month);
        String report = formatReport(emp, month, total);
        emailer.send("reports@corp.com", report);  // side effect hidden in generate()
        return report;
    }
}
 
// GOOD — testable; EmailService is injected; can be replaced with a fake in tests
public class SalesReport {
    private final EmailService emailService;
 
    public SalesReport(EmailService emailService) {
        this.emailService = emailService;
    }
 
    public String generate(Employee emp, Month month) {
        double total = sumSales(emp, month);
        String report = formatReport(emp, month, total);
        emailService.send("reports@corp.com", report);
        return report;
    }
}
 
// Test — no SMTP server needed
@Test
void reportIncludesEmployeeNameAndTotal() {
    FakeEmailService fakeEmail = new FakeEmailService();
    SalesReport salesReport = new SalesReport(fakeEmail);
 
    String result = salesReport.generate(employee("Alice Smith"), Month.JANUARY);
 
    assertThat(result).contains("Alice Smith");
    assertThat(result).contains("$4,250.00");
    assertThat(fakeEmail.getSentCount()).isEqualTo(1);
}

C++ equivalent:

// GOOD — pure virtual IEmailService makes SalesReport testable
class IEmailService {
public:
    virtual ~IEmailService() = default;
    virtual void send(std::string_view to, std::string_view body) = 0;
};
 
class SalesReport {
public:
    explicit SalesReport(std::unique_ptr<IEmailService> emailService)
        : emailService_(std::move(emailService)) {}
 
    std::string generate(const Employee& emp, Month month) {
        double total = sumSales(emp, month);
        std::string report = formatReport(emp, month, total);
        emailService_->send("reports@corp.com", report);
        return report;
    }
 
private:
    std::unique_ptr<IEmailService> emailService_;
};

Python equivalent:

# GOOD — EmailService is a protocol / ABC; easy to fake in tests
from abc import ABC, abstractmethod
 
class EmailService(ABC):
    @abstractmethod
    def send(self, to: str, body: str) -> None: ...
 
class SalesReport:
    def __init__(self, email_service: EmailService) -> None:
        self._email = email_service
 
    def generate(self, emp: Employee, month: Month) -> str:
        total = self._sum_sales(emp, month)
        report = self._format_report(emp, month, total)
        self._email.send("reports@corp.com", report)
        return report
 
# Test — no SMTP server needed
class FakeEmailService(EmailService):
    def __init__(self) -> None:
        self.sent: list[tuple[str, str]] = []
 
    def send(self, to: str, body: str) -> None:
        self.sent.append((to, body))
 
def test_report_includes_employee_name():
    fake_email = FakeEmailService()
    report = SalesReport(fake_email).generate(employee("Alice Smith"), Month.JANUARY)
    assert "Alice Smith" in report
    assert len(fake_email.sent) == 1

2. Rule 2: No Duplication — Code Level

Why this rule exists: Duplication is the primary enemy of a well-designed system. When the same logic exists in two places, a bug fix must be applied in two places. A behavior change must be applied in two places. Each copy is an opportunity to forget a corresponding change. Over time, copies diverge and the system becomes inconsistent. DRY (Don’t Repeat Yourself) states that every piece of knowledge should have a single, authoritative representation.

Code-level duplication is the easiest kind to spot: identical or near-identical blocks of code in different classes or methods.

// BAD — sales-summing logic duplicated across three service classes
public class SalesReportService {
    public double computeMonthlySales(Employee emp, int month) {
        double total = 0;
        for (Sale sale : emp.getSales()) {
            if (sale.getMonth() == month) total += sale.getAmount();
        }
        return total;
    }
}
 
public class CommissionService {
    public double computeMonthlySales(Employee emp, int month) {
        // Exact duplicate — if Sale ever gets a "cancelled" flag, this needs updating too
        double total = 0;
        for (Sale sale : emp.getSales()) {
            if (sale.getMonth() == month) total += sale.getAmount();
        }
        return total;
    }
}
 
public class QuotaTrackingService {
    public double sumSalesForMonth(Employee emp, Month month) {
        // Near-duplicate — same logic, different parameter type
        return emp.getSales().stream()
            .filter(s -> s.getMonth() == month.getValue())
            .mapToDouble(Sale::getAmount)
            .sum();
    }
}
 
// GOOD — extract into a single SalesCalculator utility
public class SalesCalculator {
    public double sumForMonth(Employee emp, Month month) {
        return emp.getSales().stream()
            .filter(s -> Month.of(s.getMonth()) == month)
            .mapToDouble(Sale::getAmount)
            .sum();
    }
}
 
// All three services now delegate:
public class SalesReportService {
    private final SalesCalculator calculator;
 
    public SalesReportService(SalesCalculator calculator) {
        this.calculator = calculator;
    }
 
    public double computeMonthlySales(Employee emp, Month month) {
        return calculator.sumForMonth(emp, month);
    }
}

C++ equivalent:

// GOOD — free function in a namespace eliminates duplication across services
namespace sales {
    double sumForMonth(const Employee& emp, Month month) {
        return std::accumulate(
            emp.sales().begin(), emp.sales().end(), 0.0,
            [month](double acc, const Sale& s) {
                return acc + (s.month() == month ? s.amount() : 0.0);
            });
    }
}
// All services call sales::sumForMonth() — one point of change.

Python equivalent:

# GOOD — single function in a module; all callers import it
from decimal import Decimal
from typing import Iterable
 
def sum_sales_for_month(sales: Iterable[Sale], month: Month) -> Decimal:
    return sum(
        (s.amount for s in sales if s.month == month),
        Decimal("0")
    )

3. Rule 2: No Duplication — Design Level (Template Method)

Why this rule exists: Duplication also exists at the design level: the same sequence of steps repeated across different classes, with only one or two steps varying. This kind of duplication cannot be fixed by extracting a single function — you need a structural solution. The Template Method pattern is the classic technique.

Template Method defines the skeleton of an algorithm in an abstract base class. The invariant steps are implemented in the base class. The varying steps are declared as abstract methods that subclasses fill in.

// BAD — three ReportGenerator subclasses all repeat the same overall sequence
public class SalesReportGenerator {
    public void generate() {
        fetchData();           // same in all three
        validateData();        // same in all three
        formatAsSalesReport(); // DIFFERENT
        sendByEmail();         // same in all three
        archiveReport();       // same in all three
    }
}
public class InventoryReportGenerator {
    public void generate() {
        fetchData();                 // duplicated
        validateData();              // duplicated
        formatAsInventoryReport();   // DIFFERENT
        sendByEmail();               // duplicated
        archiveReport();             // duplicated
    }
}
 
// GOOD — Template Method pattern: base class owns the sequence; subclasses fill in the blank
public abstract class ReportGenerator {
    // Template method — final so subclasses cannot change the sequence
    public final void generate() {
        List<ReportData> data = fetchData();
        validateData(data);
        String formatted = formatReport(data);   // abstract — the varying step
        sendByEmail(formatted);
        archiveReport(formatted);
    }
 
    protected abstract String formatReport(List<ReportData> data);
 
    private List<ReportData> fetchData() {
        // invariant implementation
        return reportRepository.loadPending();
    }
    private void validateData(List<ReportData> data) {
        // invariant validation logic
    }
    private void sendByEmail(String content) {
        emailService.send("reports@corp.com", content);
    }
    private void archiveReport(String content) {
        archiveService.store(content, LocalDate.now());
    }
}
 
public class SalesReportGenerator extends ReportGenerator {
    @Override
    protected String formatReport(List<ReportData> data) {
        return new SalesFormatter().format(data);
    }
}
 
public class InventoryReportGenerator extends ReportGenerator {
    @Override
    protected String formatReport(List<ReportData> data) {
        return new InventoryFormatter().format(data);
    }
}

C++ equivalent:

// GOOD — Template Method in C++ using pure virtual methods
class ReportGenerator {
public:
    virtual ~ReportGenerator() = default;
 
    // Template method — non-virtual; owns the sequence
    void generate() {
        auto data = fetchData();
        validateData(data);
        std::string formatted = formatReport(data);   // pure virtual
        sendByEmail(formatted);
        archiveReport(formatted);
    }
 
protected:
    virtual std::string formatReport(const std::vector<ReportData>& data) = 0;
 
private:
    std::vector<ReportData> fetchData() { return repository_.loadPending(); }
    void validateData(const std::vector<ReportData>& data) { /* ... */ }
    void sendByEmail(const std::string& content) { emailService_.send(content); }
    void archiveReport(const std::string& content) { archiveService_.store(content); }
 
    ReportRepository repository_;
    EmailService emailService_;
    ArchiveService archiveService_;
};
 
class SalesReportGenerator : public ReportGenerator {
protected:
    std::string formatReport(const std::vector<ReportData>& data) override {
        return SalesFormatter{}.format(data);
    }
};

Python equivalent:

# GOOD — Template Method in Python with ABC
from abc import ABC, abstractmethod
 
class ReportGenerator(ABC):
    def generate(self) -> None:
        """Template method — final sequence, invariant steps."""
        data = self._fetch_data()
        self._validate_data(data)
        formatted = self._format_report(data)   # hook — subclass fills this in
        self._send_by_email(formatted)
        self._archive_report(formatted)
 
    @abstractmethod
    def _format_report(self, data: list[ReportData]) -> str:
        """Subclasses implement this to provide their specific format."""
        ...
 
    def _fetch_data(self) -> list[ReportData]:
        return self._repository.load_pending()
 
    def _validate_data(self, data: list[ReportData]) -> None:
        if not data:
            raise ValueError("No report data available")
 
    def _send_by_email(self, content: str) -> None:
        self._email_service.send("reports@corp.com", content)
 
    def _archive_report(self, content: str) -> None:
        self._archive_service.store(content)
 
 
class SalesReportGenerator(ReportGenerator):
    def _format_report(self, data: list[ReportData]) -> str:
        return SalesFormatter().format(data)
 
 
class InventoryReportGenerator(ReportGenerator):
    def _format_report(self, data: list[ReportData]) -> str:
        return InventoryFormatter().format(data)

4. Rule 3: Expresses the Intent of the Programmer — Naming

Why this rule exists: The majority of software cost is maintenance, not initial development. Maintainers spend more time reading code than writing it. Code that hides intent forces every reader to reconstruct the author’s mental model from scratch. Code that expresses intent is self-documenting: readers can understand it without the author present.

Good naming is the simplest, highest-leverage form of expressiveness.

// BAD — intent is buried inside the condition
if (employee.hoursWorked > 40 && employee.employmentType == 1
        && employee.startDate.isBefore(LocalDate.now().minusYears(1))) {
    addToPayroll(employee, FULL_BENEFITS_CODE);
}
 
// GOOD — intent is captured in a well-named method; condition reads as a sentence
if (employee.isEligibleForFullBenefits()) {
    payroll.enrollWithFullBenefits(employee);
}
 
// The implementation details live inside isEligibleForFullBenefits()
// A future reader learns the business rule from the call site; only looks deeper if needed
public boolean isEligibleForFullBenefits() {
    return hoursWorked > 40
        && employmentType == EmploymentType.FULL_TIME
        && startDate.isBefore(LocalDate.now().minusYears(1));
}

C++ equivalent:

// GOOD — expressive naming in C++
bool Employee::isEligibleForFullBenefits() const {
    return hoursWorked_ > 40
        && employmentType_ == EmploymentType::FullTime
        && startDate_ < Clock::today() - Years{1};
}
 
if (employee.isEligibleForFullBenefits()) {
    payroll.enrollWithFullBenefits(employee);
}

Python equivalent:

# GOOD — expressive naming in Python
class Employee:
    def is_eligible_for_full_benefits(self) -> bool:
        return (
            self.hours_worked > 40
            and self.employment_type == EmploymentType.FULL_TIME
            and self.start_date < date.today() - relativedelta(years=1)
        )
 
if employee.is_eligible_for_full_benefits():
    payroll.enroll_with_full_benefits(employee)

5. Rule 3: Expresses Intent — Standard Design Patterns

Why this rule exists: Well-known design patterns carry meaning beyond their code. When you name a class OrderCommand rather than OrderAction, any developer who knows the Command pattern immediately understands: this object encapsulates a request; it has an execute() method; it may support undo. This shared vocabulary reduces explanation overhead.

Patterns communicate intent at the class level the same way good names communicate intent at the method level.

// BAD — action names are vague; no shared vocabulary
public class OrderAction { public void run() { ... } }
public class OrderVisitThing { public void doThing(Order o) { ... } }
public class OrderWrapper { public void applyThing() { ... } }
 
// GOOD — standard pattern names communicate intent immediately
// Command pattern — encapsulates a request; supports queuing and undo
public class PlaceOrderCommand implements Command {
    private final Order order;
    private final OrderService orderService;
 
    public PlaceOrderCommand(Order order, OrderService orderService) {
        this.order = order;
        this.orderService = orderService;
    }
 
    @Override
    public void execute() { orderService.place(order); }
 
    @Override
    public void undo() { orderService.cancel(order); }
}
 
// Visitor pattern — separates an operation from the object structure it operates on
public class OrderRevenueCalculator implements OrderVisitor {
    @Override
    public void visit(StandardOrder order) {
        revenue += order.getSubtotal();
    }
    @Override
    public void visit(SubscriptionOrder order) {
        revenue += order.getMonthlyRate() * order.getRemainingMonths();
    }
}

6. Rule 3: Expresses Intent — Tests as Documentation

Why this rule exists: A well-written unit test is one of the most reliable forms of documentation: it cannot go stale (it will fail if the behavior changes), and it shows exactly what behavior the system is supposed to exhibit, with concrete inputs and expected outputs.

Comments explaining behavior can lie. Tests that verify behavior cannot.

// BAD — test names tell you nothing; assertion messages are absent
@Test void test1() { ... }
@Test void test2() { ... }
@Test void test3() { ... }
 
// GOOD — test names read as specifications; each test documents one behavior
@Test
void employeeWithMoreThan40HoursAndOneYearTenureIsEligibleForFullBenefits() {
    Employee emp = new EmployeeBuilder()
        .withHoursWorked(41)
        .withStartDate(LocalDate.now().minusYears(2))
        .withEmploymentType(EmploymentType.FULL_TIME)
        .build();
 
    assertThat(emp.isEligibleForFullBenefits()).isTrue();
}
 
@Test
void newHireIsNotEligibleForFullBenefitsRegardlessOfHours() {
    Employee emp = new EmployeeBuilder()
        .withHoursWorked(60)
        .withStartDate(LocalDate.now().minusMonths(3))
        .withEmploymentType(EmploymentType.FULL_TIME)
        .build();
 
    assertThat(emp.isEligibleForFullBenefits()).isFalse();
}

Reading these test names is like reading a specification document. They document every edge case the author considered.


7. Rule 4: Minimal Classes and Methods

Why this rule exists: Rules 1-3 can be taken too far. Rule 1 does not mean 100% test coverage of every trivial getter. Rule 2 does not mean extracting a method for every two-line repetition. Rule 3 does not mean creating a pattern for every concept. Over-abstraction adds cognitive load, navigation burden, and code to maintain — without delivering value.

Rule 4 is the counter-weight: keep the system as small as possible while still satisfying rules 1-3. Every class and method should justify its existence.

// BAD — over-abstracted for a trivial mapping operation
public interface EmployeeNameFormatter {
    String format(Employee employee);
}
public class FullNameFormatter implements EmployeeNameFormatter {
    @Override public String format(Employee e) { return e.getFirstName() + " " + e.getLastName(); }
}
public class EmployeeNameFormatterFactory {
    public EmployeeNameFormatter create(FormatType type) { ... }
}
// Three classes to do: firstName + " " + lastName
 
// GOOD — one method is enough; no additional abstraction needed
public String fullName() {
    return firstName + " " + lastName;
}

The rule is not “never extract” — it is “extract when you have a reason”: duplication, complexity, or a need to substitute different behavior. Do not extract preemptively.


Comparison / Summary Table

PriorityRuleCore TechniqueBenefit
1Runs all the testsTDD, small classes, DI for seamsProves the system works; enables refactoring without fear
2No duplicationExtract Method, Extract Class, Template Method, DRYSingle point of change; eliminates inconsistency drift
3Expresses intentGood names, small methods, standard patterns, tests as docsReduces maintenance cost; enables faster onboarding
4Minimal classes/methodsAvoid premature abstraction; justify every extractionReduces cognitive load and navigation burden

When to Apply / Common Exceptions

Apply DRY aggressively when:

  • The same logic appears in 2 or more places and is nontrivial
  • A future change to the logic would need to be applied in multiple places
  • The duplicated code is the same concept (not just coincidentally similar code)

Exception — coincidental similarity: Two methods that happen to produce the same result for different conceptual reasons should NOT be merged into one. Merging them couples two unrelated concepts. The test: if the business reason for changing them could ever differ, they are separate concepts.

Apply Template Method when:

  • Multiple classes share the same sequence of steps but differ in one or two steps
  • The invariant steps are non-trivial and you want a single place to maintain them

Apply Rule 4 (minimal classes) as a brake when:

  • You find yourself creating abstract base classes before you have two concrete subclasses
  • You find yourself extracting utility classes with one method
  • You find yourself naming patterns before the abstraction has proven its value

Checklist

  • Does every class and method have a test that verifies its behavior?
  • Can every class be instantiated in a test without a running database, network, or file system?
  • Is the same logic expressed in exactly one place in the codebase?
  • When you see a repeated sequence of steps, have you considered the Template Method pattern?
  • Do your class and method names communicate business intent, not implementation detail?
  • Do your tests read like specifications? Can a new team member understand the system’s behavior from the test names alone?
  • Is every class and method earning its existence? Is there a reason to extract that you could articulate to a colleague?

Key Takeaways

  1. The four rules of simple design are a complete framework: if your code runs all its tests, has no duplication, expresses intent, and minimizes classes and methods, it is simple — by definition.
  2. Rule 1 is the foundation: without tests, refactoring toward rules 2-4 is guesswork. Tests are the safety net that enables all other improvement.
  3. Duplication is the primary enemy: every duplication is a future inconsistency waiting to happen. DRY is non-negotiable.
  4. Template Method is the standard technique for removing design-level duplication (same sequence, varying step).
  5. Naming is the highest-leverage expressiveness technique: isEligibleForFullBenefits() is worth a hundred comments.
  6. Standard patterns communicate intent: naming a class with a pattern name gives every reader who knows the pattern an instant, accurate mental model.
  7. Tests are the best documentation: they cannot go stale, they are always precise, and they express exactly what the system should do.
  8. Rule 4 is the counter-weight: simple does not mean tiny. It means no unnecessary complexity. Every class and method must justify its existence.

  • ch10-classes — SRP and cohesion; the conditions that make Rule 1 achievable
  • ch09-unit-tests — F.I.R.S.T. principles; tests as first-class code
  • ch11-systems — Separation of construction from use; DI as the enabler of testability
  • ch03-functions — Do One Thing; functions that are small enough to test trivially

Last Updated: 2026-04-14