Design by Contract in Java with Examples
Introduction
This tutorial explains Design by Contract (DbC) and how to apply it in Java, which has no built-in support for it. We will start with the idea of a contract between a method and its callers, and its three parts: preconditions, postconditions and class invariants. We will then implement a leave account whose contract is checked in code, and see each kind of violation reported - including one caused by a subclass that breaks its parent's promise. Next, we will look at how contracts work with inheritance. Lastly, we will cover the ways to express contracts in Java, and how DbC differs from defensive programming.What is Design by Contract
Bertrand Meyer introduced Design by Contract in the 1980s, and built it into his Eiffel language. The idea comes from business contracts: each party has obligations and gets benefits. A method (the supplier) and the code that calls it (the client) agree on -- Preconditions: what must be true when the method is called. Ensuring them is the caller's obligation, and the method may assume them. For example, the number of leave days requested must be positive and not more than the balance.
- Postconditions: what the method guarantees when it returns, provided the preconditions held. Ensuring them is the method's obligation, and the caller may rely on them. For example, the balance is reduced by exactly the days taken.
- Class invariants: what is always true of every object of the class - after construction, and before and after every public method call. For example, the balance is never negative and never above the yearly allowance.
Why contracts help
A contract answers three questions that otherwise live only in people's heads -- What may I pass? The preconditions are the method's real input rules, beyond what the parameter types say.
- What will I get? The postconditions say exactly what the method does, which is also exactly what its tests should check.
- Whose bug is it? A failed precondition is a bug in the caller; a failed postcondition or invariant is a bug in the method. When a contract check fails, you know where to look.
Example - a leave account with a checked contract
Java has norequire or ensure keywords, so the code below uses three small helper methods to make each kind of check readable. LeaveAccount.take() checks its preconditions on entry, and its postcondition and the class invariant before it commits the change. A subclass with a bug then shows what a postcondition check catches.
Java code showing preconditions, postconditions and a class invariant for a leave account
//DesignByContract.java
public class DesignByContract {
// Small helpers that make the contract checks readable
static final class Contract {
static void require(boolean condition, String rule) { // the caller's obligation
if (!condition) throw new IllegalArgumentException("Precondition failed: " + rule);
}
static void ensure(boolean condition, String rule) { // the method's obligation
if (!condition) throw new AssertionError("Postcondition failed: " + rule);
}
static void invariant(boolean condition, String rule) { // always true
if (!condition) throw new AssertionError("Invariant broken: " + rule);
}
}
static class LeaveAccount {
private final int allowance;
private int balance;
LeaveAccount(int allowance) {
Contract.require(allowance > 0, "allowance > 0");
this.allowance = allowance;
this.balance = allowance;
Contract.invariant(balanceInRange(balance), "0 <= balance <= allowance");
}
/**
* Requires: 0 < days <= balance().
* Ensures: balance() == old balance() - days.
*/
void take(int days) {
Contract.require(days > 0, "days > 0, but was " + days);
Contract.require(days <= balance,
"days <= balance (" + balance + "), but was " + days);
int newBalance = deduct(balance, days);
Contract.ensure(newBalance == balance - days, "balance == old balance - days");
Contract.invariant(balanceInRange(newBalance), "0 <= balance <= allowance");
balance = newBalance; // committed only when the contract holds
}
int deduct(int from, int days) { return from - days; }
int balance() { return balance; }
private boolean balanceInRange(int value) { return 0 <= value && value <= allowance; }
}
// A subclass with a bug: it rounds leave up to whole weeks of 5 days
static class WeeklyLeaveAccount extends LeaveAccount {
WeeklyLeaveAccount(int allowance) { super(allowance); }
@Override
int deduct(int from, int days) { return from - ((days + 4) / 5) * 5; }
}
static void attempt(String label, Runnable action) {
try {
action.run();
System.out.println(label + " -> ok");
} catch (IllegalArgumentException | AssertionError e) {
String error = e.getClass().getSimpleName();
System.out.println(label + " -> " + error + ": " + e.getMessage());
}
}
public static void main(String[] args) {
LeaveAccount tom = new LeaveAccount(20);
attempt("Tom takes 3 days ", () -> tom.take(3));
System.out.println("Tom's balance: " + tom.balance());
attempt("Tom takes 0 days ", () -> tom.take(0));
attempt("Tom takes 30 days", () -> tom.take(30));
LeaveAccount harry = new WeeklyLeaveAccount(20);
attempt("Harry takes 3 days", () -> harry.take(3));
System.out.println("Harry's balance: " + harry.balance());
}
}
Tom takes 3 days -> ok Tom's balance: 17 Tom takes 0 days -> IllegalArgumentException: Precondition failed: days > 0, but was 0 Tom takes 30 days -> IllegalArgumentException: Precondition failed: days <= balance (17), but was 30 Harry takes 3 days -> AssertionError: Postcondition failed: balance == old balance - days Harry's balance: 20
Explanation of the code
- The Javadoc comment on
take()states the contract for readers; theContractcalls enforce it at runtime. Ideally the two say the same thing. - Precondition failures: taking 0 days or 30 days is the caller's mistake.
require()throwsIllegalArgumentException, the standard Java exception for a bad argument, and the message states the rule that was broken and the value that broke it. - Postcondition failure:
WeeklyLeaveAccountoverridesdeduct()and takes 5 days instead of 3. The caller did nothing wrong, so the check throws anAssertionError- the conventional signal for a bug in the code itself. - Because
take()computesnewBalanceand checks it before assigning it, the failed call leaves Harry's balance at 20. A contract check is most useful when it stops a bad state from being stored. - The constructor establishes the invariant, and
take()preserves it. Since the field is private and onlytake()changes it, those two checks are enough to guarantee the invariant for the object's whole life.
Contracts and inheritance
The bug inWeeklyLeaveAccount is a contract violation by a subclass. Callers of LeaveAccount.take() rely on the parent's contract, whatever subclass they actually receive. So an overriding method may accept more or promise more, but never demand more or promise less.
Ways to express contracts in Java
- Javadoc: document preconditions with
@paramand@throws, and postconditions with@return. It is the contract every reader sees, even if nothing checks it. - Exceptions for preconditions of public methods:
Objects.requireNonNull(),Objects.checkIndex()andIllegalArgumentException. These checks must always run, because callers of a public API cannot be trusted - Effective Java recommends checking parameters this way. assertfor postconditions and invariants: anassertstatement documents an internal assumption and checks it when assertions are enabled withjava -ea, typically in development and tests. They are off by default, so they cost nothing in production - and so they must never be used for input validation.- Record compact constructors: the natural home for a value's invariant, since every instance passes through them.
- Bean Validation: in Jakarta EE and Spring, annotations such as
@NotNull,@Positiveand@Sizeon parameters and return values declare simple contracts that a framework checks. - Types: the strongest contract is one the compiler checks. A parameter of type
EmployeeIdinstead ofString, orOptional<Employee>for a result that may be missing, puts part of the contract into the signature.
Design by Contract and defensive programming
The two approaches answer the question "who checks?" differently. Defensive programmingClick to read tutorial: Fail Fast and Defensive Programming in Java says each method protects itself against bad input. Design by Contract says the precondition is the caller's responsibility, so the method does not need to code around it - checking it is a way to catch the caller's bug, not to recover from it. In practice the two combine well: at a trust boundary, such as a public API or user input, check defensively and reject bad data; inside, state contracts, check them with assertions or cheap checks, and treat any failure as a bug to fix rather than a case to handle.Summary
In this tutorial we first saw Bertrand Meyer's Design by Contract and its three parts: preconditions are the caller's obligation, postconditions are the method's obligation, and class invariants are always true. Next we saw how a contract documents a method and tells you whose bug a failure is. We then built a leave account whose contract is checked in code, and saw precondition failures for bad requests and a postcondition failure caused by a buggy subclass - with the bad balance never stored. We then saw how contracts must behave under inheritance, which is the Liskov Substitution Principle in contract form. Lastly, we listed the ways to express contracts in Java, and compared DbC with defensive programming.Programming Principles Tutorials
Fail Fast and Defensive Programming in JavaClick to read tutorial: Fail Fast and Defensive Programming in Java Principle of Least Astonishment in API DesignClick to read tutorial: Principle of Least Astonishment in API Design SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC
Fail Fast and Defensive Programming in JavaClick to read tutorial: Fail Fast and Defensive Programming in Java Principle of Least Astonishment in API DesignClick to read tutorial: Principle of Least Astonishment in API Design SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples DRY, KISS and YAGNI Principles with Java ExamplesClick to read tutorial: DRY, KISS and YAGNI Principles with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC