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

@Requires

a method or constructor; the closure may use the parameters and the fields

PreconditionViolation

@Ensures

a method or constructor; the closure may use the parameters, the fields, result and old

PostconditionViolation

@Invariant

a class; the closure may use the fields only

ClassInvariantViolation

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 defensive if.

  • The postcondition of push mixes the new state (peek(), items.size()) with old.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) and old, the field values on entry, read as old.fieldName.

  • old holds copies, and Groovy only knows how to copy some field types: primitives and their wrappers, BigInteger, BigDecimal, strings and GString`s, and fields whose class is `Cloneable. That is why the stack above declares its list as ArrayList rather than List: List is an interface that is not Cloneable, 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), with groovy.contracts for the annotations and org.apache.groovy.contracts for the violation classes.

  • Loop invariants, @Decreases, @ThrowsIf, @Modifies and the unwoven-precondition members are recent additions (the documentation labels @Decreases, @Modifies, @ThrowsIf and the @Requires members incubating) and may be refined in a later release.

  • Contracts are woven with Java assertion semantics, so -da turns them off; Groovy’s own assert statement is always on.