← chrisjmendoza.com Sample chapter · Reading the Machine

From the book's front matter

How this book was written

The code came first and the writing followed it — with a lot of machine help in both. The two applications were built with heavy AI coding assistance, and so was this book: the chapters were drafted in collaboration with AI models, working directly against the two repositories, under rules written down before drafting began.

What you should judge is the discipline, not the drafting. Every listing is copied from the repositories at the pinned commits and re-checked by an audit script that compares the printed text against the source, line by line; every count in the prose was measured by a command rather than remembered, and re-measured before release; and the full text has been through independent technical review. Where this book teaches you to trust the artifact over the author and your terminal over the text, it is describing its own editorial method, applied to itself.

If that provenance changes how much you trust a sentence here, good — that is the correct instinct, and the remedy is the one Chapter 1 teaches: check. Everything in this book is checkable, on purpose.

← Back to chrisjmendoza.com

Chapter 3

Invariants and the Canonical Unit

contracts, normalisation, and boundaries

All model geometry stored in millimeters… Convert to inches only at UI edges… Never store or calculate geometry in inches.” That’s .github/copilot-instructions.md, bolded, near the top. CLAUDE.md and CONTRIBUTING.md state the identical rule near their own tops too, each in its own words — “canonical millimeters,” “unit conversion … happens only at the UI edge.” Same non-negotiable, three documents, three different phrasings. Rules that get restated that often, in that many voices, are load-bearing. This chapter is about what kind of thing that rule is, why it’s worth writing rules like that down, and what happens to a program that doesn’t have any.

In this chapter you will

  • learn what an invariant is and how it differs from a validation check
  • see why normalising to a canonical form at the boundary simplifies everything inside
  • meet preconditions, postconditions, and design by contract
  • understand why parsing and formatting live at the edges of a program, never in the middle
  • learn to recognise the anti-corruption layer pattern wherever it appears

3.1 The most expensive bug in engineering software

In 1999 NASA lost the Mars Climate Orbiter. The spacecraft’s trajectory software received thruster impulse data in pound-force seconds; it expected newton-seconds. Nobody made an arithmetic error. Every individual calculation was correct. The system simply had two units circulating in it, and no single place that said which one was true.

ShaftSchematic has exactly that hazard. The machinists on the client’s shop floor work in inches and think in fractions; marine engineering documents are frequently metric. The app has to accept both, display either, and never confuse them — across ~39,000 lines, eighteen model files, four PDF composers, and two independent rendering paths.

The solution the codebase lands on is the standard one, and it has a name.

3.2 Canonical form

Canonicalisation means: pick one representation, convert everything to it at the moment it enters the system, and use only that representation internally.

ShaftSchematic’s canonical form is the millimetre. Every geometric field in the model is named to say so — lengthMm, diaMm, startFromAftMm, keywayOffsetFromEndMm. The suffix isn’t decoration. It’s the unit assertion carried in the identifier, so that a mistake is visible at every use site, not just at the declaration.

lengthMm is a field name and a tiny act of self-defence.

THE MILLIMETRE COREmodel/ · geom/ · ViewModel · renderers · PDF composersShaftSpecoverallLengthMmBodydiaMmTaperlengthMmno inch value ever exists in hereText input"7 1/2"Dimension label7.500 inGrid legend1 inPDF calloutØ 4.250parseToMm()fromMillimeters()conversion lives only on these two dashed lines
Figure 3.1 — The millimetre core. Inches exist only outside the two dashed lines — in text the user typed, and in text drawn onto a screen or a page.

Note what the diagram does not show: any arrow that carries inches into the core. By design there isn’t one — the sanctioned boundary is exactly two functions, parseToMm on the way in and UnitSystem.fromMillimeters on the way out. Grep for the underlying constant instead of the two function names, though, and the picture gets more interesting — see the Pinch Point below.

Shop-floor analogy

Every shop has one set of master gauge blocks, kept in a temperature-controlled case and treated as the final word on a dimension, and everything else in the shop traces back to them. Not because inches are better than millimetres, but because one reference beats two. When an inspector and a machinist disagree about a measurement, they don’t argue about it — they both go to the same blocks. UnitSystem.MILLIMETERS is ShaftSchematic’s gauge block set: the one reference everything else is checked against.

3.3 What an invariant is

Here is the precise definition, and it’s worth memorising:

An invariant is a condition that is true before and after every operation on a piece of state, for the entire lifetime of that state.

Not “checked once at startup.” Not “validated on save.” Always true. If you could freeze the program at any instant and inspect memory, the invariant would hold.

“All geometry in the model is in millimetres” is an invariant of ShaftSpec.

Compare that with Body.isValid() from the last chapter. That’s a validation check — a function you call at a moment of your choosing to ask whether a particular condition currently holds. Validation is a question. An invariant is a promise.

The distinction matters because it changes where the work goes:

Validation Invariant
Enforced by calling a function design of the boundary
Can be violated? yes, then detected never, if the boundary is complete
Cost one check per call site one conversion per entry point
Failure mode forgot to call it a new entry point nobody thought about

Look at the cost row. Validation costs you a check everywhere. An invariant costs you a conversion at the border. In a program with two hundred call sites and two borders, the invariant is a hundred times cheaper — which is the whole reason the technique exists.

It’s also, in a very literal sense, what a contractor is being paid for. A client doesn’t ask for an invariant — they state a rule, in plain language, and hand it to you: never store or calculate geometry in inches. Encoding that rule into the shape of the data, so it can’t be forgotten rather than merely checked, is a large part of what the fee is for.

Chew on this

An invariant is only as strong as the completeness of its boundary. What would break the millimetre invariant in ShaftSchematic? Think about it before reading on. Not “someone writes bad code” — think about new features. A new import format. A CSV paste. An API. Each is a new door into the core, and each one has to be taught to convert. This is why the rule is written in CLAUDE.md, CONTRIBUTING.md, and copilot-instructions.md: those documents are the boundary being defended in prose, because it can’t be defended in code.

3.4 Preconditions, postconditions, and contracts

An invariant is one of three related promises, and it is worth having all three names because they attach to different moments.

Together they are called design by contract, and the framing is exactly what it sounds like: two parties, each owing the other something specific. If the caller meets the precondition, the function owes the postcondition. If the caller doesn’t, the function owes nothing at all — including a sensible error.

Kotlin gives you three ways to state a contract, and both codebases use all of them:

Mechanism Checked Example
The type system at compile time pattern: Pattern cannot be null
require / check at run time, throwing require(modelClass.isAssignableFrom(...))
Documentation by human beings the file headers and KDoc

ShaftViewModelFactory shows the middle one:

require(modelClass.isAssignableFrom(ShaftViewModel::class.java)) {
    "Unsupported ViewModel type: $modelClass"
}

That is a precondition stated in code. require throws IllegalArgumentException — the caller passed something wrong. Its sibling check throws IllegalStateExceptionthis object is in a state where the operation makes no sense. The distinction is about whose fault it is, and choosing correctly is a message to whoever reads the stack trace at two in the morning.

Nobody has ever read a stack trace at a reasonable hour.

Now look at a postcondition stated in prose, from ShaftSpec.withBodyAt:

“Returns a copy with the body at index set to the given geometry, or this unchanged when index is out of range. Length and Ø are clamped to ≥ 0; startMm is kept verbatim (authored positions are sacred — golden rule). Keyway fields, id, and label are untouched.”

Four guarantees about the returned value, none of which the type system can express — ShaftSpec is just ShaftSpec. So they live in KDoc, and WithBodyAtTest is what actually enforces them. That pairing is the practical form design by contract usually takes when the type system can’t express the rule: the contract is written in the doc comment and enforced by the test.

Chew on this

Notice that withBodyAt has no precondition on index. An out-of-range index is not an error; it returns this. That’s a deliberate choice with a real trade-off.

A require(index in bodies.indices) would catch a caller bug loudly, at the moment it happens. Returning this makes the function total — defined for every input — which means callers never have to guard, and a race between a delete and an edit degrades to a no-op instead of a crash. For a UI-driven edit path, silent tolerance is probably right. For a batch importer, loud failure probably is.

The question to ask is never “should I validate?” but “who is better placed to handle this being wrong — me, or my caller?”

Pinch point — the contract only holds at the boundary you defend

Design by contract has a failure mode: a contract nobody can check is a wish. The sanctioned boundary for the millimetre invariant is two functions and three documents — but grep -rn "25\.4" app/src/main turns up the same conversion, reimplemented, in well over a dozen other files. Three of them even redeclare their own private copy of the constant under three different names: pdf/ShaftPdfComposer.kt has its own MM_PER_IN, ui/viewmodel/SessionAddDefaults.kt has MM_PER_INCH, ui/viewmodel/SnapUtils.kt has INCH_TO_MM — none of the three calling parseToMm or fromMillimeters at all. Every one of those sites happens to compute 25.4 correctly today. But “happens to” is precisely the failure mode an invariant exists to remove, and a boundary that has quietly grown a dozen side doors is a boundary in name only. Push the scenario one step further: someone adds a CSV importer that writes Float inches straight into a Body. Nothing throws, nothing fails to compile, and no test goes red. The drawing is simply wrong by a factor of 25.4 — the same factor already loose in the building.

This is why §3.3 said an invariant is only as strong as the completeness of its boundary. Preconditions and postconditions defend functions; invariants defend data, and data has more doors than this one keeps finding.

3.5 The conversion itself

Here is the entire unit system:

util/UnitSystem.kt — the entire enum — package line omitted

@kotlinx.serialization.Serializable
enum class UnitSystem(val displayName: String) {
    INCHES("Inches") {
        override fun toMillimeters(value: Double): Double = value * 25.4
        override fun fromMillimeters(value: Double): Double = value / 25.4
    },
    MILLIMETERS("Millimeters") {
        override fun toMillimeters(value: Double): Double = value
        override fun fromMillimeters(value: Double): Double = value
    };

    abstract fun toMillimeters(value: Double): Double
    abstract fun fromMillimeters(value: Double): Double
}

This is worth a slow read, because it’s doing something more sophisticated than it looks.

What this file is actually doing

  1. It’s an enum, so there are exactly two units and the compiler knows it.
  2. Each constant has a body that overrides two abstract methods. This is legal Kotlin and it means each enum constant is effectively its own subclass.
  3. The class declares the methods abstract, which forces every constant — present and future — to supply both directions. Add CENTIMETERS tomorrow and the code will not compile until you’ve written both conversions.
  4. MILLIMETERS implements both as identity. It converts nothing. But it exists, which means calling code never has to branch on which unit it has.

That fourth point is the good part. Compare with a version that’s easy to reach for first:

// The version this design avoids
fun toMm(value: Double, unit: UnitSystem): Double =
    if (unit == UnitSystem.INCHES) value * 25.4 else value

Functionally identical today. But the if is a conditional on type, and every one you write is a place that must be found and edited when a third case appears. The enum-with-bodies version has zero conditionals: you call unit.toMillimeters(v) and the right implementation runs. This is the Strategy pattern, and the “replace conditional with polymorphism” refactoring is one of the most reliably valuable moves in the catalogue.

Questions from the floor

Q: Isn’t MILLIMETERS.toMillimeters() a pointless function call? A: It’s a real call, and it does nothing, and that’s the point — it’s a null object: an implementation that satisfies the contract by doing nothing, so callers never need a special case. The cost is one virtual dispatch; the benefit is that no call site anywhere contains the word if.

Q: Why Double here, when the model uses Float? A: Deliberate. Conversion is where precision loss compounds, so it’s done in the wider type and narrowed once, at assignment into the model. Parsing (parseToMm) also returns Double. This is a small, correct decision that’s easy to get wrong.

Q: Why is UnitSystem @Serializable at all, if the model is always mm? A: Because the user’s preferred display unit is part of a saved document — reopening a shaft you drew in inches should show inches. That’s UI state, not geometry, so it rides along in the envelope without violating anything. Chapter 12 looks at that envelope.

3.6 Parsing: turning human text into canonical data

The inbound edge is more interesting than the outbound one, because humans type strange things. The parser has to accept, from a machinist with oily hands on a phone screen:

12        1.25        3/4        15 1/2        4.250"        6 mm

Here it is:

util/Parsing.kt — trimmed

fun parseToMm(raw: String, unit: UnitSystem): Double {
    val v = parseFractionOrDecimal(raw) ?: return 0.0
    return if (unit == UnitSystem.MILLIMETERS) v else v * MM_PER_IN
}

fun parseFractionOrDecimal(raw: String): Double? {
    val t = normalizeNumericText(raw)
    if (t.isEmpty()) return null

    // Mixed fraction: W N/D
    val parts = t.split(' ').filter { it.isNotBlank() }
    if (parts.size == 2 && parts[1].contains('/')) {
        val whole = parts[0].toDoubleOrNull() ?: return null
        val frac = parseSimpleFraction(parts[1]) ?: return null
        return if (whole < 0) whole - frac else whole + frac
    }

    // Simple fraction: N/D
    if (t.contains('/')) return parseSimpleFraction(t)

    return t.toDoubleOrNull()
}

Three structural observations, each of which generalises far beyond this file.

First: it’s a pipeline of narrowing. normalizeNumericText strips commas, trailing unit suffixes, and stray whitespace. Only then does anything try to interpret the result as a number. Separating cleaning from interpreting is why this function is readable at all; a version that handled quotes and fractions in the same pass would be a thicket.

Second: failure is a value, not an exception. parseFractionOrDecimal returns Double? — null for “I couldn’t.” No exception is thrown. The caller decides what “couldn’t” means, and parseToMm decides it means zero. Text arriving from a human is expected to be malformed sometimes; malformed input is not an exceptional circumstance, so it isn’t an exception.

Third, and note this one — the KDoc states a deliberate non-responsibility:

“This keeps parsing neutral: do not clamp negatives or enforce ranges here. Callers (e.g., ViewModel setters) can layer validation if needed.”

That is a separation of concerns decision written down at the point where somebody would otherwise be tempted to violate it. Parsing answers “what number is this text?” Validation answers “is that number allowed here?” They are different questions with different right answers in different contexts, and fusing them produces a parser that can only be used in one place.

One more thing worth noticing before moving on. parseToMm’s own body — return if (unit == UnitSystem.MILLIMETERS) v else v * MM_PER_IN — is exactly the conditional-on-type shape §3.5 named “the version this design avoids.” The sanctioned boundary doesn’t call unit.toMillimeters(v), the strategy object living one file away; it branches instead. With exactly two units, forever, that costs nothing today. But spotting that kind of inconsistency — a pattern named as the thing to avoid two pages back, sitting unremarked in the one function that guards the actual boundary — is exactly the reading skill this book is training.

Pinch point — the parser is more permissive than you’d bet money on

normalizeNumericText strips trailing non-numeric characters by walking backwards from the end until it finds an allowed one. So 4.250" becomes 4.250 — good. And 4.250abc also becomes 4.250, which is probably fine.

But trace 12x7. The walk starts at the last character, 7, which is allowed — so it stops immediately, nothing is stripped, and "12x7".toDoubleOrNull() returns null. The string fails to parse and parseToMm yields 0.0. Reasonable behaviour, but not what most readers predict on a first reading, and not what the function’s name suggests.

The lesson isn’t that this is a bug — for a shop-floor numeric field it is defensible. The lesson is that a permissive parser’s exact behaviour on malformed input is rarely obvious from reading it. Behaviour that is hard to predict from the source is behaviour that belongs in a test, where it becomes an executable specification. ParsingTest.kt pins the mixed-fraction and unit-suffix cases; the three above are worth adding to it.

3.7 Formatting: the outbound edge

Going out is a different problem. LengthFormat.formatInchesSmart() doesn’t just divide by 25.4 — it has to render a number the way a machinist expects to read it:

data class InchFormatOptions(
    val maxDenominator: Int = 16,
    val snapToleranceInches: Double = 1e-4, // ~0.0001" ≈ 0.0025 mm
    val decimalPlaces: Int = 3
)

The logic: try to express the value as a fraction with denominator up to 16; if the value is within 0.0001″ of such a fraction, print the fraction (½, ¾, ⅝ — it even has a Unicode lookup table); otherwise print three decimal places.

Look at snapToleranceInches and think back to the float-equality warning in Chapter 2. A value that entered as 15 1/2 inches became 393.7 mm, was stored as a Float, was maybe involved in arithmetic, and now has to come back out as 15 1/2 and not 15.500. Exact equality would fail. The tolerance is the round-trip guarantee. Without it, typing a fraction and reading it back would produce a decimal, and the app would feel subtly broken to the exact user it’s built for.

On the bench

Open util/LengthFormatTest.kt. Find a test case involving a value that is not representable as a sixteenth — something like 0.3 inches. Write down what the test says the output should be. Then answer: what would the user see if snapToleranceInches were changed from 1e-4 to 1e-2? Would that be better or worse, and for whom?

3.8 The general pattern: an anti-corruption layer

Step back from units for a moment. The shape you’ve just seen is completely general, and it appears three more times across the two applications:

Boundary Outside representation Canonical inside Converter
Numeric input "15 1/2" (text, inches) Float mm parseToMm
Saved document JSON envelope ShaftSpec ShaftDocCodec
SMS provider content://sms cursor rows MessageEntity SmsSyncHandler
Reaction fallback Loved 'hello' (a text message) ReactionEntity ReactionFallbackParser

Every one of these is the same move: the outside world has a representation you don’t control, so you convert it once, at a named place, into a representation you do control, and everything downstream only sees yours.

That named place has a name of its own: an anti-corruption layer. The term comes from Domain-Driven Design and the metaphor is accurate — without it, the outside world’s model leaks inward and corrupts yours. A Postmark that passed raw content://sms cursors around would have Android’s schema, Android’s null rules, and Android’s MMS/SMS ID collisions spread through every screen. Instead it has MessageEntity, and the mess is confined to data/sync/.

Questions from the floor

Q: Isn’t this just a lot of extra copying? A: Yes. Deliberately. You’re spending CPU cycles and a bit of memory to buy a guarantee about where a class of bugs can live. That’s usually a spectacular trade — cycles are cheap and debugging is not — but it is a trade, and there are systems (very high throughput, very tight memory) where it isn’t worth it. Knowing it’s a trade is the professional part.

Q: Postmark’s MMS_ID_OFFSET = 10_000_000_000L — is that an invariant too? A: It’s an invariant in the making. The rule “MMS rows are stored at rawMmsId + MMS_ID_OFFSET” must hold everywhere or IDs collide with SMS rows. Notice that it’s enforced by convention and comment rather than by type — a data class MmsId(val raw: Long) would make it impossible to forget. That’s the same primitive obsession smell from Chapter 2, in a place where it’s genuinely consequential. For now, the offset is doing the job of a type system that hasn’t been hired yet.

Spec sheet

  • Canonicalisation: choose one internal representation, convert at the boundary, never carry two.
  • A precondition is the caller’s obligation before a call; a postcondition is the function’s guarantee after it; an invariant holds throughout. Together: design by contract.
  • require blames the caller (IllegalArgumentException); check blames the object’s state (IllegalStateException). Pick the one that tells the truth to whoever reads the stack trace.
  • When the type system can’t express a contract, it usually ends up written in KDoc and enforced by a test instead.
  • An invariant is always true, for the whole lifetime of the state. A validation check is a question you ask at a chosen moment.
  • Invariants cost one conversion per boundary; validation costs one check per call site. That ratio is why invariants scale.
  • An invariant is only as good as the completeness of its boundary — which is why the mm rule is written in three docs. Prose is the enforcement mechanism when code can’t be.
  • Naming fields lengthMm carries the unit assertion to every use site, not just the declaration.
  • UnitSystem uses enum constants with bodies to replace a conditional with polymorphism; MILLIMETERS is a null object whose conversions do nothing so that callers never branch.
  • Conversion and parsing use Double and narrow to Float once — precision loss is confined to one place.
  • Parsing returns a nullable, not an exception. Malformed human input is expected, not exceptional.
  • Keep parsing separate from validation: “what number is this?” and “is that number allowed?” are different questions.
  • The general pattern is an anti-corruption layer: convert an external representation you don’t control into an internal one you do, at exactly one named place.

Cold bench — orientation

Every other exercise in this book hands you a codebase two people have already spent months making sense of on your behalf. This one doesn’t, and it’s the first of five — one roughly every third chapter, each picking up where the last one left off.

Pick a Kotlin Android app on GitHub: a few hundred commits at least, more than one contributor if you can find it, and — the important part — one you have never opened before. Not a tutorial repo. Not a fork of something you already half-know. Keep it; the next cold bench assumes you still have it cloned. Set a timer for twenty minutes and don’t go over.

  • Clone it. Don’t read anything yet — just look at the top-level package folders, the way §1.4 had you look at ShaftSchematic’s model/ and geom/ before reading either.
  • Find the entry point: one Activity, Application, or MainActivity — whatever the platform hands control to first.
  • Write down five noun-like names from the top level, the things this app is about, before you’ve read a line of logic.
  • Pick one of those nouns, open its file, and note what kind of type it is — data class, plain class, interface, something else.
  • Find one rule this app enforces that isn’t merely “the code compiles” — a value that’s always normalised, a field that’s never negative, two flags that are never both true. Skim for a require, a check, or a comment that reads like a promise; you don’t need the whole file.
  • Write one sentence naming exactly where that rule is enforced, the way §3.2 could point at two named functions for the millimetre rule.

No answer key. Nobody has read this repository on your behalf and written down what you’re supposed to find — that’s rather the exercise.

Further reading

  • Meyer, Object-Oriented Software Construction (2nd ed. 1997) — design by contract stated in full, by the person who named it, in a language built around the idea rather than bolting it on.
  • Evans, Domain-Driven Design (2003) — names the anti-corruption layer as a pattern, and is honest about the organisational reasons a system ends up needing one.
  • Gamma, Helm, Johnson & Vlissides, Design Patterns (1994) — Strategy, which is what UnitSystem’s enum constants with bodies are doing when they replace a conditional with polymorphism.

Review questions

  1. Define invariant and validation check, and explain why the two have different costs as a program grows.
  2. State ShaftSchematic’s central invariant in one sentence, and name the two functions that form its boundary.
  3. Why does UnitSystem.MILLIMETERS implement toMillimeters() as an identity function instead of the enum simply having no method for that case?
  4. What refactoring is UnitSystem an example of, and what does that refactoring buy you when a third unit is added?
  5. parseFractionOrDecimal returns Double? rather than throwing. Justify that choice in terms of what kind of event malformed input is.
  6. The KDoc on parseToMm explicitly forbids clamping negatives. What principle is being defended, and what would be lost if the rule were ignored?
  7. Explain the purpose of snapToleranceInches in LengthFormat, connecting it to the way Float equality behaves.
  8. Give three examples of an anti-corruption layer from the two applications, naming the outside representation and the canonical inside one.
  9. Why is the millimetre rule stated in three separate documentation files rather than enforced in code? What would enforcing it in code even look like?
  10. MMS_ID_OFFSET keeps MMS IDs from colliding with SMS IDs. Describe how you would move that rule from convention into the type system, and one cost of doing so.
  11. Define precondition, postcondition, and invariant, and say which party each obligates.
  12. require and check throw different exception types. Explain the distinction and why it matters to whoever reads the failure.
  13. ShaftSpec.withBodyAt deliberately has no precondition on index — an out-of-range value returns this. Give one argument for that choice and one against, and state the general question that decides it.