Design by Contract with groovy-contracts
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). Design by contract describes a piece of code by what it demands from its callers
(preconditions), what it promises in return (postconditions) and what must always be true of an object between calls (class
invariants). The groovy-contracts module, which ships in the Groovy distribution (lib/groovy-contracts-6.0.0.jar), turns
those three ideas into annotations whose arguments are closures; an AST transformation then weaves the checks into the
compiled class. @Requires, @Ensures, @Invariant (on classes and on loops) and @Contracted are the stable core; @Decreases,
@Modifies, @ThrowsIf and the woven/direct members of @Requires are the newer parts, which the Groovy 6.0.0
documentation and API mark as incubating and say may be refined in later releases. All of them existed in the 6.0.0
distribution and behaved as described below in the runs.
The three annotations
All contract annotations live in groovy.contracts; the exceptions they throw live in org.apache.groovy.contracts, and all
of them extend AssertionError, so a violation is an Error, never an exception to be caught in ordinary control flow.
| Annotation | Placed on | Violation thrown |
|---|---|---|
|
a method or constructor; the closure may use the parameters and the fields |
|
|
a method or constructor; the closure may use the parameters, the fields, |
|
|
a class; the closure may use the fields only |
|
A bounded stack
The official documentation introduces the module with a stack. The same idea, written for this page as a stack with a capacity
(so that push has something to demand), shows every basic feature at once.
import groovy.contracts.*
import org.apache.groovy.contracts.*
@Invariant({ capacity > 0 && items.size() <= capacity })
class BoundedStack<T> {
final int capacity
final ArrayList<T> items = []
@Requires({ capacity > 0 })
@Ensures({ isEmpty() })
BoundedStack(int capacity) { this.capacity = capacity }
boolean isEmpty() { items.isEmpty() }
boolean isFull() { items.size() == capacity }
@Requires({ !isFull() })
@Ensures({ peek() == item && items.size() == old.items.size() + 1 })
void push(T item) { items << item }
@Requires({ !isEmpty() })
T peek() { items.last() }
@Requires({ !isEmpty() })
@Ensures({ result == old.items.last() })
T pop() { items.removeLast() }
}
def s = new BoundedStack<String>(2)
s.push('a'); s.push('b')
println "top = ${s.peek()}, full = ${s.isFull()}"
try { s.push('c') } catch (PreconditionViolation e) { println e.message }
println s.pop()
println s.pop()
try { s.pop() } catch (PreconditionViolation e) { println e.message }
try { new BoundedStack<String>(0) } catch (PreconditionViolation e) { println e.message }
top = b, full = true
<groovy.contracts.Requires> BoundedStack.push(java.lang.Object):void
!isFull()
||
|true
false
b
a
<groovy.contracts.Requires> BoundedStack.pop():java.lang.Object
!isEmpty()
||
|true
false
<groovy.contracts.Requires> BoundedStack.<init>(int)
capacity > 0
| |
0 false
The violation message is a power-assert diagram (see Assertions & Power Assert) headed by the annotation, the class and the method signature, so the failing condition and the value of each sub-expression are visible without a debugger. Three points are worth reading off the example:
-
A precondition is checked on entry, so the caller is blamed:
push('c')on a full stack fails before the method body runs and the body never needs a defensiveif. -
The postcondition of
pushmixes the new state (peek(),items.size()) withold.items.size(), the size the list had when the call started. -
The class invariant is checked around each method call, so a public method can rely on it when it starts and must restore it before it returns.
Contracts on the greenhouse Schedule
The running greenhouse scenario has a Schedule of plants to water. Its rules are exactly the kind of thing that gets lost in
comments: no blank or duplicate plant, at most maxPerDay entries, and cancel only for a plant that is on the list.
(Here Schedule is a plain class so that the contracts stand out; the watering DSL that builds it is described on
Domain Specific Languages.)
import groovy.contracts.*
import org.apache.groovy.contracts.*
@Invariant({ maxPerDay > 0 && entries.size() <= maxPerDay })
class Schedule {
final int maxPerDay
final ArrayList<String> entries = []
@Requires({ maxPerDay > 0 })
Schedule(int maxPerDay) { this.maxPerDay = maxPerDay }
@Requires({ plant?.trim() && !entries.contains(plant) && entries.size() < maxPerDay })
@Ensures({ entries.contains(plant) && entries.size() == old.entries.size() + 1 })
void water(String plant) { entries << plant }
@Requires({ entries.contains(plant) })
@Ensures({ !entries.contains(plant) })
void cancel(String plant) { entries.remove(plant) }
// a deliberately wrong implementation: the postcondition catches it
@Ensures({ result >= 0 && result < entries.size() })
int lastIndex() { entries.size() } // off by one
}
def s = new Schedule(2)
s.water('tomatoes')
s.water('basil')
println s.entries
['', 'tomatoes', 'mint'].each { name ->
try { s.water(name) } catch (PreconditionViolation e) { println "${name ?: '<blank>'}: ${e.class.simpleName}" }
}
s.cancel('basil')
println s.entries
try { s.cancel('basil') } catch (AssertionViolation e) { println e.class.simpleName + ' is an AssertionError: ' + (e instanceof AssertionError) }
s.water('basil')
try { s.lastIndex() } catch (PostconditionViolation e) { println e.message }
// entries is exposed, so a caller can break the invariant behind the class's back
s.entries << 'mint' << 'sage'
try { s.cancel('mint') } catch (ClassInvariantViolation e) { println e.message }
[tomatoes, basil]
<blank>: PreconditionViolation
tomatoes: PreconditionViolation
mint: PreconditionViolation
[tomatoes]
PreconditionViolation is an AssertionError: true
<groovy.contracts.Ensures> Schedule.lastIndex():int
result >= 0 && result < entries.size()
| | | | | | |
2 true | 2 | | 2
false | ['tomatoes', 'basil']
false
<groovy.contracts.Invariant> Schedule
maxPerDay > 0 && entries.size() <= maxPerDay
| | | | | | |
2 true| | 3 | 2
| | false
| ['tomatoes', 'basil', 'sage']
false
The blank name, the duplicate and the third plant (the schedule is full) each fail the same compound precondition; the
diagram of a real failure would show which clause was false. The off-by-one lastIndex is reported by its postcondition with
result shown as 2. The last case shows the limit of an invariant: it protects the object against its own methods, not
against a caller who mutates an exposed field directly. In that run the violation was reported after the cancel call had
removed mint (three entries against a limit of two), so the stricter fix is to stop exposing the mutable list.
Closure rules, result and old
The closures are ordinary Groovy but are restricted by what they are allowed to see, and the module checks the rules at
compile time or, for old, by what it can copy:
-
An invariant may refer to fields only. A precondition may refer to fields and to the method’s parameters. A postcondition may also use
result(the returned value) andold, the field values on entry, read asold.fieldName. -
oldholds copies, and Groovy only knows how to copy some field types: primitives and their wrappers,BigInteger,BigDecimal, strings andGString`s, and fields whose class is `Cloneable. That is why the stack above declares its list asArrayListrather thanList:Listis an interface that is notCloneable, so no copy is made.
import groovy.contracts.*
class A {
List<Integer> readings = [1] // declared type List: not copied for old
@Ensures({ old.readings != null })
void add(int r) { readings << r }
}
try { new A().add(2); println 'ok' } catch (Throwable e) { println e.class.simpleName }
PostconditionViolation
With ArrayList<Integer> as the declared type the same contract passes. If a postcondition dereferences the missing copy
(old.readings.size()) the failure is a NullPointerException inside the generated check, which is harder to read than a
violation, so prefer concrete, copyable field types in classes that use old.
Inheritance of contracts
A class that implements an interface or extends a class inherits that type’s invariants, preconditions and postconditions, so what clients were told to expect is still checked on every implementation.
import groovy.contracts.*
import org.apache.groovy.contracts.*
interface Waterable {
@Requires({ litres > 0 })
@Ensures({ result == litres })
int water(int litres)
}
@Invariant({ tank >= 0 })
class Zone implements Waterable {
int tank = 10
int water(int litres) { tank -= litres; litres } // contracts come from the interface
}
class Bench extends Zone {
@Override
int water(int litres) { super.water(litres * 2) } // breaks the inherited postcondition
}
def z = new Zone()
println z.water(4) + ' litres, tank = ' + z.tank
try { z.water(0) } catch (PreconditionViolation e) { println 'z.water(0): precondition (inherited from Waterable)' }
try { z.water(50) } catch (ClassInvariantViolation e) { println 'z.water(50): class invariant' }
try { new Bench().water(1) } catch (PostconditionViolation e) { println 'Bench.water(1): postcondition (inherited)' }
4 litres, tank = 6
z.water(0): precondition (inherited from Waterable)
z.water(50): class invariant
Bench.water(1): postcondition (inherited)
Zone.water carries no annotation of its own, yet the interface’s precondition and postcondition are enforced on it and on
the Bench override. This is what makes an annotated interface a usable contract: every implementation is checked against
the same text.
@Contracted, disabling checks and compatibility
@Contracted marks a class (or, on a package-info, a package) as one that takes part in contract processing. In the
examples on this page the explicit @Requires, @Ensures and @Invariant annotations were enough and @Contracted was not
needed; a class that also carries it behaves the same.
import groovy.contracts.*
@Contracted
class Sensor {
@Requires({ celsius > -50 })
double toKelvin(double celsius) { celsius + 273.15 }
}
println new Sensor().toKelvin(20)
try { new Sensor().toKelvin(-80) } catch (AssertionError e) { println e.class.simpleName }
293.15
PreconditionViolation
The woven checks use Java assertion machinery, so the standard switches apply. Running the bounded-stack example with
JAVA_OPTS="-da" (disable assertions) printed top = b, full = true and then let push('c') succeed and the following
pop() return c: no violation was reported at all. The usual practice is the same as with Java assertions, namely to keep
contracts on in tests and development and make a deliberate decision before running without them in production, because
contracts must never be the only thing standing between a caller and corrupt data.
The module also works together with @CompileStatic: a class annotated with both still throws PreconditionViolation for a
failing @Requires in a run with Groovy 6.0.0.
Contracts in scripts
Groovy contracts intentionally do not work on top-level script code in the traditional form. With Groovy 5 and later the
JEP 445 style of script (instance main method and methods declared next to it) works, and the annotations are moved to the
generated script class.
import groovy.contracts.*
import org.apache.groovy.contracts.*
@Requires({ litres > 0 })
@Ensures({ result == litres * 60 })
def seconds(int litres) { litres * 60 }
def main() {
println seconds(2)
try { seconds(0) } catch (PreconditionViolation e) { println 'rejected: ' + e.class.simpleName }
}
120
rejected: PreconditionViolation
Loop invariants
@Invariant can also be placed on a for, while or do-while loop (Groovy 6.0). The condition is checked at the start
of each iteration and a violation throws LoopInvariantViolation, again an AssertionError. The closure may use any variable
in scope, including the loop variable, but must not assign. Several @Invariant annotations can be stacked on one loop.
import groovy.contracts.*
import org.apache.groovy.contracts.*
int tank = 10
try {
@Invariant({ tank >= 0 })
while (tank != 0) { tank -= 3 } // 10, 7, 4, 1, -2: overshoots zero
} catch (LoopInvariantViolation e) { println e.message }
println 'tank = ' + tank
int sum = 0
@Invariant({ sum >= 0 })
@Invariant({ sum <= 100 })
for (int i in 1..5) { sum += i }
println "sum = $sum"
<groovy.contracts.Invariant> loop invariant
tank >= 0
| |
-2 false
tank = -2
sum = 15
A condition such as tank > 0 would not have caught the bug above, because the loop would simply have stopped; the
invariant is a statement about every iteration, independent of the loop’s own test.
Termination with @Decreases (incubating)
@Decreases declares a variant: a value that must strictly decrease on each loop iteration or each recursive re-entry of a
method, so progress toward termination is checked rather than hoped for. A number must stay non-negative as it decreases; a
list of comparable values is compared lexicographically, which suits nested loops. A violation throws LoopVariantViolation
for loops and RecursionVariantViolation for methods. The official documentation marks @Decreases as incubating in 6.0.0.
import groovy.contracts.*
import org.apache.groovy.contracts.*
int n = 3
try {
@Decreases({ n })
while (n > 0) { n-- }
println 'finished, n = ' + n
@Decreases({ n })
while (true) { /* never changes n */ if (n++ > 5) break }
} catch (LoopVariantViolation e) { println 'variant: ' + e.class.simpleName }
@Decreases({ n })
long factorial(int n) { n <= 1 ? 1 : n * factorial(n - 1) }
@Decreases({ n })
int stuck(int n) { n == 0 ? 0 : stuck(n) } // same argument on every call
println factorial(5)
try { stuck(3) } catch (RecursionVariantViolation e) { println 'recursion: ' + e.class.simpleName }
finished, n = 0
variant: LoopVariantViolation
120
recursion: RecursionVariantViolation
Unwoven preconditions
A precondition is woven by default, meaning the module generates the check and a violating caller gets a
PreconditionViolation. Code that already guards itself (a hand-written if, Objects.requireNonNull, a validation library)
can keep its guard and still state the obligation, with woven = false: nothing is generated and the caller sees whatever the
existing check throws. The woven and direct members are incubating in 6.0.0; direct is pure information for tools, and
most code never sets it.
Choosing between an unwoven @Requires and @ThrowsIf over the same guard is a statement of intent. @Requires says a
violating call is the caller’s bug; @ThrowsIf (next section) says the throw is defined behaviour that callers may rely on and
catch.
Exceptional contracts with @ThrowsIf (incubating)
@ThrowsIf describes a method’s defined failures: it throws the given exception when the condition holds on entry. It is
repeatable, with one condition and exception type per annotation. By default the annotation is the guard (the check is woven
at the method’s entry); with woven = false it only documents a throw that exists in the body; with checked = true a
verification wrapper reports a body that does not honour the contract with ThrowsIfViolation. The example puts the
unwoven @Requires and both @ThrowsIf styles on a greenhouse pump.
import groovy.contracts.*
import org.apache.groovy.contracts.*
class Pump {
int tank = 10
// woven = false: the obligation is documented, the existing check stays in charge
@Requires(value = { rate != null }, woven = false)
String describe(Integer rate) {
Objects.requireNonNull(rate)
"rate $rate"
}
// woven guard: the annotation is the check, and the exception is defined behaviour
@ThrowsIf(value = { litres > tank }, exception = IllegalStateException)
void draw(int litres) { tank -= litres }
// documents an existing throw; checked = true verifies the body against it
@ThrowsIf(value = { litres < 0 }, exception = IllegalArgumentException, woven = false, checked = true)
int refill(int litres) { tank += litres } // forgot the guard
}
def p = new Pump()
println p.describe(5)
try { p.describe(null) } catch (Throwable e) { println 'describe(null): ' + e.class.simpleName }
p.draw(4)
println p.tank
try { p.draw(99) } catch (IllegalStateException e) { println 'draw(99): IllegalStateException (defined behaviour)' }
println p.refill(2)
try { p.refill(-1) } catch (ThrowsIfViolation e) { println 'refill(-1): ' + e.class.simpleName }
rate 5
describe(null): NullPointerException
6
draw(99): IllegalStateException (defined behaviour)
8
refill(-1): ThrowsIfViolation
The unwoven precondition left the library’s NullPointerException in place, the woven @ThrowsIf delivered the declared
IllegalStateException, and the checked arm turned the missing guard in refill into a violation instead of silently
accepting a negative refill. When a @Requires and a @ThrowsIf both apply to the same call, the precondition violation is
reported first.
Frame conditions with @Modifies
@Modifies states which fields (written this.field) or parameters a method may change; everything else is declared
unchanged. It generates no runtime check. Its value is as a specification for readers and tools, plus a compile-time
validation: the named fields and parameters must exist, and when an @Ensures is present its old may only mention fields
that @Modifies lists.
import groovy.contracts.*
class Bed {
ArrayList<String> plants = []
int watered = 0
@Modifies({ this.plants })
@Ensures({ old -> old.plants.size() < plants.size() })
void plant(String name) { plants << name }
}
println new Bed().tap { plant('basil') }.plants
[basil]
Referring to a field that is not in the frame is rejected when the class is compiled. Adding old.watered == watered to the
postcondition above stops the script with:
e9b.groovy: 7: @Ensures references old.watered but @Modifies does not declare 'watered' as modifiable @ line 7, column 5.
To check the method body against the frame, the groovy-typecheckers module (also in the distribution) provides the
ModifiesChecker type-checking extension. Here a method that declares only items but also increments count fails to
compile.
import groovy.contracts.*
import groovy.transform.TypeChecked
class Sloppy {
int count
List<String> items = []
@TypeChecked(extensions = 'groovy.typecheckers.ModifiesChecker')
@Modifies({ this.items })
void add(String s) { items << s; count++ }
}
e9_modifies_checker.groovy: 19: [Static type checking] - @Modifies violation: assignment to 'count' but 'count' is not declared in @Modifies
@ line 19, column 38.
The compile-time @Ensures check works with the checker alone; type-checking extensions in general are covered on
Type Checking Extensions and
Built-in Type Checkers.
Contracts and tests
Contracts and tests answer different questions. A test fixes a few inputs and checks the outputs; a contract is checked on
every call the program makes, including those from the tests, so a good test suite exercises the contracts for free. Because
a violation is an AssertionError, a test that wants to prove a precondition is enforced can use shouldFail(PreconditionViolation)
from groovy.test.GroovyAssert, as the official examples do. The relationship to unit tests is developed in
Testing with JUnit & Groovy Test and
Testing with Spock & Geb; the Java side of testing is on
Java Testing.
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) predates the groovy-contracts module, which is now part of the distribution and has grown since: do not expect the book to cover design by contract as described here, and treat it as background on assertions and closures only. What to take from the current documentation instead:
-
The module is
groovy-contracts(ships with Groovy 6.0.0), withgroovy.contractsfor the annotations andorg.apache.groovy.contractsfor the violation classes. -
Loop invariants,
@Decreases,@ThrowsIf,@Modifiesand the unwoven-precondition members are recent additions (the documentation labels@Decreases,@Modifies,@ThrowsIfand the@Requiresmembers incubating) and may be refined in a later release. -
Contracts are woven with Java assertion semantics, so
-daturns them off; Groovy’s ownassertstatement is always on.