Skip to content

Metadata Card

  • Prerequisites: Familiarity with at least one programming language
  • Estimated Time: 45 minutes
  • Core Difficulty: Foundation-building
  • Completion Milestone: Master static checking, defensive programming, assertions, and the design-by-contract mindset

Your Progress

You arrived in the City of Artisans. Every craftsman here—every artisan—knows that the quality of a finished blade depends on the quality of the raw material, the accuracy of the blueprint, and the reliability of the measuring tools. A single loose bolt can bring down the entire forge.

You stand in front of your first forge: a blank code editor. Your Task

This chapter is the foundation stone of Volume VI. It's not teaching you new syntax—it's a framework for thinking: how do you write code that you can trust before it even runs? Static checking, defensive programming, assertions, and design-by-contract are the four measuring tools of the artisan. They transform you from "code that happens to work most of the time" to "code that is correct by construction."

Chapter tiers

  • Required reading: Static checking vs dynamic checking, defensive programming, assertions
  • Selective: Design-by-contract, pre/post-conditions, invariants
  • Advanced: Immutability and thread safety

This chapter won't require you to master

  • Formal verification and theorem provers
  • JML (Java Modeling Language) syntax

Breaking Ground · Tracing the Source

A few months ago you wrote a class Tournament:

java
public class Tournament {
    private String name;
    private int maxPlayers;
    // ... getters and setters
}

Everything worked fine. Then one day a colleague wrote this code in another module:

java
Tournament t = new Tournament();
t.setName(null);
t.setMaxPlayers(-5);

No compilation error. No runtime exception. The system performed normally for a while, until it performed a null operation on t.getName().length() — and crashed.

Who's at fault? The colleague who wrote the null—or the Tournament class that didn't guard against it?

In the City of Artisans, if you build a forge, you don't just hand someone the raw iron and hope they use it correctly. You forge a measuring tool into the blueprint itself—a specification that says "this value must not be null, this range must be positive."

First Layer: Static Checking — Catching Errors Before They Run

Static checking means the compiler catches your errors before the code ever executes. This is the first measuring tool in the artisan's kit.

java
// Java example — the compiler catches this
// String name = null; — the compiler knows NullPointerException is possible
// int value = "hello"; — type mismatch, caught at compile time

// Compile-time vs Runtime
List<String> names = new ArrayList<>();
names.add("Alice");
String name = names.get(0); // no cast needed — compiler knows it's a String

Static type systems are the first line of defense. But not all languages are the same. Even within the same language, you can choose to lean more on static or dynamic checking.

The Java compiler checked the types for you. The Python interpreter would let None travel three hundred lines before crashing at the worst possible moment. This is why the City of Artisans prefers static checking for structural connections—you want to know a gear fits before you weld it in.

Static vs Dynamic:

java
// Java — verifies at compile time
public Player findPlayer(String id) {
    // Returns Player, not null (hopefully)
}
python
# Python — "it works until it doesn't"
def find_player(id):
    # Could return None, could crash
    pass
# find_player(...).something() — ZeroDivisionError? AttributeError? Who knows!

The general principle: push as many errors as possible from runtime to compile time. Every bug caught by the compiler is one you don't have to find with a debugger at 2 AM.

Not all languages have strong static type systems. For those that don't (Python, JavaScript), the principle still applies: write code that is structured so that errors are detectable before execution. Use type hints (mypy in Python, TypeScript instead of JavaScript), linting tools, and code reviews.

Second Layer: Defensive Programming — Expect the Unexpected

Defensive programming means: never trust external input, never trust the caller, never trust yourself three months ago.

Your Tournament class needs to protect its invariants:

java
// Defensive — guard your boundaries
public class Tournament {
    private final String name;
    private final int maxPlayers;

    public Tournament(String name, int maxPlayers) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Tournament name must not be null or empty");
        }
        if (maxPlayers < 2 || maxPlayers > 100) {
            throw new IllegalArgumentException("maxPlayers must be between 2 and 100");
        }
        this.name = name;
        this.maxPlayers = maxPlayers;
    }
}

Now it's impossible to create an invalid Tournament. The measuring tool is forged into the forge itself.

Key rules of defensive programming:

1. Validate all public method parameters — don't trust the caller
2. Validate all external input — files, network, user input
3. Return safe defaults when appropriate (but don't hide errors)
4. Never ignore exceptions — even if you log and rethrow
5. Fail fast — detect problems as early as possible

The "fail fast" principle deserves emphasis:

java
// Bad — fail slow, fail confusing
public Player findPlayer(String id) {
    // ... 100 lines of logic ...
    // ... returns null on line 101 ...
}

// Good — fail fast, fail clear
public Player findPlayer(String id) {
    if (id == null) throw new IllegalArgumentException("id must not be null");
    // ... 100 lines ...
}

The earlier you detect a problem, the closer you are to its root cause. A null pointer exception at the construction site tells you exactly where the bad data entered, not where it finally collapsed.

Third Layer: Assertions — Documenting Invariants That Must Hold

Assertions are executable checks that document "this must be true at this point." They're your internal correctness guarantees.

java
// Assertion examples
public class Match {
    private int scoreA, scoreB;
    private boolean finished;

    public void finish() {
        assert !finished : "Match already finished";
        this.finished = true;
        // ...
    }

    public void addScore(Player player, int points) {
        assert !finished : "Cannot add score to a finished match";
        assert points > 0 : "Points must be positive, got " + points;

        if (player == Player.A) {
            scoreA += points;
        } else {
            scoreB += points;
        }
        assert scoreA >= 0 && scoreB >= 0;
    }
}

Assertions in Java are disabled by default (enable with -ea). In practice, assertions work well in development and testing but are often stripped from production. That's fine—their primary value is during development.

Unit tests back up assertions, but the two serve different roles:

AssertionsUnit Tests
When they runAt runtime (if enabled)At build time (ideally)
What they checkInvariant conditionsBehavioral correctness
Who reads themDevelopers maintaining this codeAnyone verifying the code works
What they document"This cannot happen here""This scenario produces this output"

Both are needed. Assertions document internal guarantees; tests validate external behavior.

Fourth Layer: Design-by-Contract — Preconditions and Postconditions

Design-by-Contract (DbC) formalizes the relationship between a method and its caller as a contract. Bertrand Meyer introduced this concept in the Eiffel language.

The contract has three parts:

Precondition: What the caller must guarantee
               (input is valid, object is in correct state)

Postcondition: What the method guarantees on return
               (output is valid, object state transformed correctly)

Invariant: What must always be true for this class
            (no illegal state, internal constraints hold)
java
/**
 * Contract for transferScout:
 *
 * Precondition:
 *   - scoutId must be registered
 *   - fromOutpost and toOutpost must exist and be different
 *   - scout must currently be assigned to fromOutpost
 *
 * Postcondition:
 *   - scout is assigned to toOutpost
 *   - scout's outpost change log has a new record
 *   - fromOutpost no longer lists this scout
 *   - toOutpost now lists this scout
 */
public void transferScout(String scoutId, String fromOutpost, String toOutpost) {
    // implementation
}

The key insight: the contract makes clear who is responsible for what. If the precondition fails (caller didn't provide valid input), the caller is at fault. If the postcondition fails (method didn't deliver), the method is at fault. This clarity reduces blame-shifting in debugging sessions.

Contracts and defensive programming:

  • Defensive programming: the method checks everything itself (double work, but safe)
  • Design-by-contract: rely on the caller to meet preconditions (efficient, but risky)

Choose based on the context:

  • For internal methods called by trusted code: DbC is sufficient
  • For public APIs called by external code: defensive programming is safer
  • For safety-critical systems: both, always

Common Pitfalls

Pitfall 1: Defensive to the point of absurdity. Checking name != null is good. Checking name != null && !name.isEmpty() && name.length() < 256 && name.matches("[a-zA-Z]+") && name.equals(HTMLUtils.escape(name)) in a method that's only called internally by your own team—that's too much. Calibrate. Accept that some code is called by trusted callers only.

Pitfall 2: Assertions that have side effects. assert list.remove("x") — if assertions are disabled in production, this removal doesn't happen. Never put real logic in assertions.

java
// Wrong — assertion has side effect
assert cache.evictOldEntries();

// Right — logic is separate
boolean evicted = cache.evictOldEntries();
assert evicted : "Cache eviction failed for no apparent reason";

Pitfall 3: Silent error handling. Catching Exception and doing nothing, or returning null instead of throwing. The most insidious bugs are silent ones:

java
// Silent — the caller has no idea something went wrong
try {
    processMatch(matchId);
} catch (Exception e) {
    // absorbed — help desk asks "why is the match not updating?"
}

// Better
try {
    processMatch(matchId);
} catch (Exception e) {
    log.error("Failed to process match {}", matchId, e);
    throw e; // don't hide it
}

Pitfall 4: No design-by-contract thinking at method boundaries. Every method has an implicit contract. Not documenting it is like not telling your customer the delivery date—they'll assume it's "immediately" and blame you when it's "three days."


Passing Challenges

  • Warm up: Add defensive checks to a method you wrote recently. Check all parameters, fail fast on invalid input, throw descriptive exceptions.
  • Challenge: Add Java assertions (or equivalent in your language) to a class you maintain. Use assertions to document invariants—things that must be true after every method call.
  • Observe: Look at open-source Java libraries (Guava, Apache Commons). Count how many methods have if (argument == null) throw new NullPointerException(...) as their first lines. Why do they all do this?

Traveler's Notes

Software construction is a mindset—the craftsman's measuring tools before you touch the forge. Static checking catches errors at compile time, defensive programming protects against the unexpected, assertions document internal guarantees, and design-by-contract formalizes the relationship between caller and callee. Master these four, and the rest of your craftsmanship has a solid foundation.


Preview of Next Chapter

Now that you know how to write code that is correct by construction, the next question is: how do you organize data so that it's impossible to misuse? We move from "how to build a forge" to "how to arrange your raw materials." Next: Abstract Data Types.

Built with VitePress | Software Systems Atlas