Style Guide & Idioms: The 21 Official Rules

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); the outputs shown are the real ones. Groovy accepts almost all Java syntax, so a Java developer can write Java in a .groovy file and it works. The official style guide lists 21 habits that make that code more idiomatic: what to drop, what to use instead, and a few warnings. This page goes through all 21 in the guide’s order, with an original before-and-after example for each, and ends with CodeNarc, a third-party tool that checks many of them automatically. The examples follow the greenhouse used across this section.

The official guide calls itself incomplete and says it is a quick introduction, not a standard. The rules are advice, and the guide itself says several are a matter of taste (return, parentheses). Adopt them as a team, write the choices down, and enforce them with a tool rather than in code review.

The 21 rules

1. No semicolons

Semicolons are optional in Groovy, and leaving them out is the idiomatic style. Java code pasted into a Groovy file is usually full of them. A semicolon is only needed to put two statements on one line.

// before: Java habit
String crop = "basil";
int days = 14;
System.out.println(crop + " grows in " + days + " days");

// after
String crop2 = 'basil'
int days2 = 14
println "$crop2 grows in $days2 days"

// the one place a semicolon is still needed: two statements on one line
int a = 1; int b = 2
println a + b
basil grows in 14 days
basil grows in 14 days
3

2. Return keyword optional

The value of the last expression evaluated in a method body is returned, so return can be left out. It reads best in short methods and closures. When the last line is a bare variable that was just computed on the line above, a return (or a blank line before it) can be clearer; the guide says it is a matter of taste. The caution is about methods declared with def: the last expression may be something you did not mean to return, such as the value of an assignment. Prefer a concrete return type or void. Even if/else and try/catch produce a value when they are the last statement.

// before
int doubled(int x) { return x * 2 }
// after
int tripled(int x) { x * 3 }
println doubled(4)
println tripled(4)

// the trap: with def, the value of the last assignment is returned
def addKey(Map m) { m.size = 3 }
println addKey([:])
// a concrete return type makes the intent explicit
Map addKeyTyped(Map m) { m.size = 3; m }
println addKeyTyped([:])

// if/else as the last statement is the return value
String height(int cm) { if (cm > 100) 'tall' else 'short' }
println height(120)
8
12
3
[size:3]
tall

3. Def and type

Use def or a type, not both: def String x is redundant. def means a variable of static type Object. Untyped parameters do not need def, and a constructor does not need it either. The guide adds that typing method parameters is usually better (see rule 21).

// before: def and a type together
def String species = 'basil'
def int addDays(def a, def b) { a + b }
class PlantA { def PlantA(String n) { println "built $n" } }

// after
String species2 = 'basil'
int addDays2(int a, int b) { a + b }       // typed parameters, no def
int addDays3(a, b) { a + b }               // or untyped, still no def
class PlantB { PlantB(String n) { println "built $n" } }

println species2
println addDays2(2, 3)
println addDays3(2, 3)
new PlantA('kale')
new PlantB('kale')
basil
5
5
built kale
built kale

4. Public by default

Classes and methods are public unless you say otherwise, so the public modifier is noise. Write a modifier only when the member is not public. Package-private visibility has no keyword in Groovy; the @PackageScope annotation provides it.

import groovy.transform.PackageScope
import java.lang.reflect.Modifier

// before
public class BedA {
    public String describe() { 'bed' }
}
// after
class BedB {
    String describe() { 'bed' }
    private String secret() { 'hidden' }
    @PackageScope String zone() { 'north' }
}

println Modifier.isPublic(BedA.getMethod('describe').modifiers)
println Modifier.isPublic(BedB.getMethod('describe').modifiers)
println Modifier.toString(BedB.getDeclaredMethod('secret').modifiers)
println "[" + Modifier.toString(BedB.getDeclaredMethod('zone').modifiers) + "]"
true
true
private
[]

5. Omitting parentheses

A top-level call with arguments, such as println, does not need parentheses. When a closure is the last argument, put it after the call and drop the empty parentheses: of the three ways of writing each below the last is the preferred one. Parentheses are still required for nested calls and for calls without arguments, where the bare name would be read as a property.

println('hello')                   // before
println 'hello'                    // after

[1, 2].each({ print "$it " })      // closure inside the parentheses
println()
[1, 2].each() { print "$it " }     // closure outside, empty parentheses
println()
[1, 2].each { print "$it " }       // preferred
println()

// still required: nested calls and calls without arguments
String s = 'basil'
println s.toUpperCase()
try {
    println s.toUpperCase          // read as a property
} catch (MissingPropertyException e) {
    println "${e.class.simpleName}: ${e.property}"
}
hello
hello
1 2
1 2
1 2
BASIL
MissingPropertyException: toUpperCase

6. Classes as first-class citizens

A class name is an expression, so the .class suffix is not needed, as with Java’s instanceof. Class literals also combine well with GStrings.

// before
Class c1 = String.class
// after
Class c2 = String
println c1.is(c2)

def plant = 'basil'
println plant instanceof String
println "type of plant: ${plant.getClass()}"
println "String is ${String.simpleName}, List is ${List.name}"
true
true
type of plant: class java.lang.String
String is String, List is java.util.List

7. Getters and Setters

A getter and setter pair is a property, and Groovy lets you read and write it with field-like syntax. When you declare your own classes, a field with no modifier is a property: the compiler generates the private field, the getter and the setter, and Java code sees the usual methods. If you write a getter or setter yourself, the compiler uses yours.

// before: the Java bean
class JavaStyleSensor {
    private String id
    String getId() { id }
    void setId(String id) { this.id = id }
}
// after: a property
class Sensor { String id }

def js = new JavaStyleSensor()
js.setId('s1')
println js.getId()

def s = new Sensor()
s.id = 's1'                  // calls setId
println s.id                 // calls getId
println Sensor.declaredMethods*.name.findAll { it in ['getId', 'setId'] }.sort()

// your own logic replaces the generated one
class Valve {
    private int level
    int getLevel() { level }
    void setLevel(int l) { level = Math.max(0, Math.min(l, 100)) }
}
def v = new Valve()
v.level = 150
println v.level
s1
s1
[getId, setId]
100

8. Initializing beans with named parameters and the default constructor

A class with a default constructor can be created with a map of property values: the constructor runs first, then the setters are called in the order given. It replaces a sequence of setter statements.

import groovy.transform.ToString

@ToString
class Bed { String crop; int rows; boolean covered }

// before
def b1 = new Bed()
b1.crop = 'kale'
b1.rows = 3
b1.covered = true
// after
def b2 = new Bed(crop: 'kale', rows: 3, covered: true)

println b1
println b2
assert b1.toString() == b2.toString()
Bed(kale, 3, true)
Bed(kale, 3, true)

9. Using with() and tap() for repeated operations on the same bean

Named parameters are for new instances. For an instance you were given, with runs a closure whose delegate is the object, so the receiver name is not repeated; the result of with is the value of the closure’s last statement. tap does the same but always returns the object itself, which suits builder-like code. The guide also notes that with(true) behaves like tap and with(false) like with.

class Pump {
    String state = 'off'
    int speed
    boolean start() { state = 'on'; true }
    void speedUp() { speed += 10 }
}

// before: repeating the receiver
def p1 = new Pump()
p1.speed = 20
p1.speedUp()
p1.start()

// after: with returns the last statement, tap returns the object
def p2 = new Pump()
def result = p2.with {
    speed = 20
    speedUp()
    start()
}
println "with returned: $result"

def p3 = new Pump().tap {
    speed = 20
    speedUp()
    start()
}
println "tap returned a ${p3.class.simpleName}: $p3.state at $p3.speed"

def p4 = new Pump().with(true) { start() }
println "with(true) returned a ${p4.class.simpleName}"
with returned: true
tap returned a Pump: on at 30
with(true) returned a Pump

10. Equals and ==

In Groovy == is a null-safe equals (and compares with compareTo for Comparable objects), while Java’s reference comparison is the is() method. Prefer == for value comparison: it never throws a NullPointerException because of a null on either side.

String a = new String('basil')
String b = new String('basil')
String n = null

// before: Java-style, with a manual null guard
println(a != null && a.equals(b))
// after
println a == b
println a.is(b)                       // reference comparison, what Java's == does
println n == 'basil'                  // null-safe
println 'basil' == n

println n.equals('basil')             // even a call on null is safe for equals

// == uses compareTo for Comparable, equals() does not
println new BigDecimal('1.0') == new BigDecimal('1.00')
println new BigDecimal('1.0').equals(new BigDecimal('1.00'))
true
true
false
false
false
false
true
false

11. GStrings (interpolation, multiline)

Double-quoted strings interpolate $name, $obj.property and ${any expression}, so concatenation with ` and `\n+ escapes is rarely needed. A ${-> expression} closure is evaluated lazily, when the GString becomes a String. Triple quotes give multiline strings, stripIndent() removes the common left indentation, and a backslash at the end of a line continues a string. Single quotes always make a plain String, so the guide’s advice is: single quotes for constants, double quotes when you rely on interpolation, and slashy strings for regular expressions so that backslashes are not doubled.

String crop = 'basil'
int days = 14

// before
String s1 = "Crop " + crop + " is ready in " + (days + 1) + " days"
// after
String s2 = "Crop $crop is ready in ${days + 1} days"
assert s1 == s2
println s2

// lazy evaluation: only the closure form sees the later change
int stock = 3
def eager = "stock: ${stock}"
def lazy = "stock: ${-> stock}"
stock = 5
println eager
println lazy

// multiline and stripIndent
String report = """\
    crop: $crop
    days: $days
    """.stripIndent()
print report

// continuation backslash: one line in the result
String long1 = 'This sentence is too long to keep on one source line, ' + \
    'so it continues here'
String long2 = 'This sentence is too long to keep on one source line, \
so it continues here'
assert long1 == long2

// slashy string: no double backslashes
assert '2026-10-14' ==~ /\d{4}-\d{2}-\d{2}/

println 'no $interpolation here'.class.simpleName
println "interpolated $crop".class.simpleName
println "plain".class.simpleName
Crop basil is ready in 15 days
stock: 3
stock: 5
crop: basil
days: 14
String
GStringImpl
String

12. Native syntax for data structures

Lists, maps, regular-expression patterns and ranges have literal syntax. Use it instead of constructing the Java collections and calling add/put.

// before
List<String> beds1 = new ArrayList<>()
beds1.add('kale'); beds1.add('basil')
Map<String, Integer> temps1 = new HashMap<>()
temps1.put('mon', 18); temps1.put('tue', 21)

// after
List<String> beds = ['kale', 'basil']
Map<String, Integer> temps = [mon: 18, tue: 21]
def pattern = ~/b\w+/
def days = 1..5
def letters = 'a'..'e'

assert beds == beds1 && temps == temps1
println beds[-1]
println temps.tue
println 'basil'.matches(pattern)
println days.sum()
println letters.join()
println beds.getClass().name
println temps.getClass().name
basil
21
true
15
abcde
java.util.ArrayList
java.util.LinkedHashMap

13. The Groovy Development Kit

The GDK decorates the JDK classes with methods such as each, find, findAll, every, collect and inject, plus many on String, File, streams and collections. They give a functional style and replace most explicit loops.

def readings = [18, 25, 31, 22, 40]

// before: an explicit loop
List<Integer> hot1 = new ArrayList<>()
for (int i = 0; i < readings.size(); i++) {
    if (readings.get(i) > 24) hot1.add(readings.get(i))
}

// after: the GDK
def hot = readings.findAll { it > 24 }
assert hot == hot1
println hot
println readings.find { it > 30 }
println readings.every { it > 10 }
println readings.collect { it * 2 }
println readings.inject(0) { acc, r -> acc + r }
readings.each { print "$it " }
println()
[25, 31, 40]
31
true
[36, 50, 62, 44, 80]
136
18 25 31 22 40

14. The power of switch

switch accepts any type, not only primitives and strings. A case value is matched with its isCase method, which the GDK defines for classes, ranges, collections, regular expressions and closures, and which your own classes can implement.

class Frost {
    boolean isCase(temp) { temp instanceof Number && temp < 0 }
}

String classify(x) {
    switch (x) {
        case 'basil':       return 'herb'
        case ~/k.*/:        return 'starts with k'
        case 0:             return 'zero'
        case 1..9:          return 'single digit'
        case [10, 20, 30]:  return 'round number'
        case new Frost():   return 'frost'
        case Integer:       return 'some other integer'
        case { it instanceof Number && it > 1000 }: return 'huge'
        default:            return 'unknown'
    }
}

['basil', 'kale', 0, 7, 20, 99, -5, 2000.5, 'oak'].each {
    println "$it -> ${classify(it)}"
}
basil -> herb
kale -> starts with k
0 -> zero
7 -> single digit
20 -> round number
99 -> some other integer
-5 -> frost
2000.5 -> huge
oak -> unknown

15. Import aliasing

import ... as Alias gives an imported class a different name, which solves clashes between same-named classes and shortens long names. Static imports can be aliased too.

import java.awt.List as AwtList
import java.util.concurrent.ConcurrentHashMap as Registry
import static java.lang.Math.max as larger

println AwtList.name
println List.name
def registry = new Registry<String, Integer>()
registry.put('kale', 3)
println registry
println larger(3, 8)
java.awt.List
java.util.List
[kale:3]
8

16. Groovy Truth

Every object can be used as a condition: null, void, zero, empty strings, empty collections and maps, and false are false, and anything else is true. A class can define its own truth with an asBoolean() method. Write if (list) instead of if (list != null && !list.isEmpty()).

List<String> beds = []
// before
if (beds != null && !beds.isEmpty()) println 'has beds' else println 'no beds'
// after
if (beds) println 'has beds' else println 'no beds'

[null, 0, 0.0, '', [], [:], 'x', 1, [0], [a: 1]].each {
    println "${it == null ? 'null' : it.inspect()} is ${it ? 'true' : 'false'}"
}

class Tank {
    int litres
    boolean asBoolean() { litres > 0 }
}
println new Tank(litres: 0) ? 'tank has water' : 'tank is empty'
println new Tank(litres: 5) ? 'tank has water' : 'tank is empty'
no beds
no beds
null is false
0 is false
0.0 is false
'' is false
[] is false
[:] is false
'x' is true
1 is true
[0] is true
['a':1] is true
tank is empty
tank has water

17. Safe graph navigation

The ?. operator returns null instead of throwing a NullPointerException when the receiver is null, so a chain of null checks collapses to one expression. The whole chain evaluates to null if any link is null.

class Sensor { Double reading }
class Zone { Sensor sensor }

Zone zone = new Zone()          // no sensor installed

// before: nested null checks
Double r1 = null
if (zone != null) {
    if (zone.sensor != null) {
        r1 = zone.sensor.reading
    }
}
// after
Double r2 = zone?.sensor?.reading

println r1
println r2

try {
    zone.sensor.reading
} catch (NullPointerException e) {
    println 'without ?. : NullPointerException'
}

zone.sensor = new Sensor(reading: 21.5)
println zone?.sensor?.reading
null
null
without ?. : NullPointerException
21.5

18. Assert

Use assert to check parameters, results and invariants. Unlike Java’s, a Groovy assert is always evaluated; it does not depend on the -ea flag. A failure prints the Power Assert report, a diagram with the value of every sub-expression. A message can follow a colon.

def zone = [name: 'north', temps: [18, 25]]

assert zone.temps.size() == 2          // passes silently

try {
    assert zone.temps.max() < 20
} catch (AssertionError e) {
    println e.message
}

try {
    int reading = -1
    assert reading >= 0 : "reading must not be negative, got $reading"
} catch (AssertionError e) {
    println e.message
}
assert zone.temps.max() < 20
       |    |     |     |
       |    |     25    false
       |    [18, 25]
       ['name':'north', 'temps':[18, 25]]
reading must not be negative, got -1. Expression: (reading >= 0). Values: reading = -1

19. Elvis operator for default values

a ?: b means “a` if it is true by the Groovy truth, otherwise b”. It is the ternary operator without the repeated middle operand, and the usual way to write a default. Since Groovy 3.0 there is also an Elvis assignment, `x ?= y.

// before
String label1(String name) { name != null ? name : 'unnamed' }
// after
String label2(String name) { name ?: 'unnamed' }

println label1(null)
println label2(null)
println label2('fern')
println label2('')                // empty is false, so the default is used

def config = [:]
config.timeout ?= 30              // Elvis assignment: set only if false
config.timeout ?= 60
println config
unnamed
unnamed
fern
unnamed
[timeout:30]

20. Catch any exception

When you do not care about the exception type, leave it out: catch (any) (the name is yours) catches every Exception, which saves a list of types. It does not catch Throwable, so an Error goes through; if you really mean everything, write catch (Throwable t).

int parse(String s) {
    try {
        Integer.parseInt(s)
    } catch (NumberFormatException | NullPointerException e) {   // before: several types, same handling
        -1
    }
}
int parse2(String s) {
    try {
        Integer.parseInt(s)
    } catch (any) {                                              // after
        -1
    }
}
println parse('x')
println parse2('x')
println parse2(null)

try {
    try {
        throw new AssertionError('boom')
    } catch (any) {
        println 'caught by catch (any)'
    }
} catch (Throwable t) {
    println "an Error escaped: $t.message"
}
-1
-1
-1
an Error escaped: boom

21. Optional typing advice

The guide’s closing advice is a rule of thumb: when code is a public API used by others, use strong typing, because it makes the contract stronger, catches wrong arguments, documents the code and helps the IDE; for code only you use, such as private methods or locals whose type is obvious, you may use def. Typing also enables the type checker described in Static Type Checking & @CompileStatic.

import groovy.transform.TypeChecked

class Doser {
    // public API: typed parameters and return type
    BigDecimal dose(BigDecimal litres, int days) { scale(litres) * days }

    // private helper: untyped is fine
    private scale(x) { x * 2 }
}
println new Doser().dose(1.5, 3)

// a wrong call to the typed API fails at the call, not deep inside
try {
    new Doser().dose('lots', 3)
} catch (MissingMethodException e) {
    println "MissingMethodException: ${e.method}"
}

// with @TypeChecked the mistake is a compile-time error
try {
    new GroovyShell().evaluate '''
        import groovy.transform.TypeChecked
        @TypeChecked
        class Bad { int f() { String s = 'a'; s.foo() } }
    '''
} catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) {
    println e.message.readLines()[1]
}
9.0
MissingMethodException: dose
Script1.groovy: 4: [Static type checking] - Cannot find matching method java.lang.String#foo(). Please check if the declared type is correct and if the method exists.

Enforcing the rules with CodeNarc

CodeNarc is a static analysis tool for Groovy source that reports defects, bad practices, inconsistencies and style issues. It is a third-party project, not part of Apache Groovy, and its releases follow their own schedule: at the time of writing (2026-10-14) the latest version on Maven Central is 4.0.0 (2026-07-18). It works on source text and does not need the code to compile, which makes it a good fit for a build or a pre-commit check.

CodeNarc rules are grouped in rule sets, and a project chooses which rules to run and with which thresholds. Many rules correspond to the guide’s advice, in particular those in the unnecessary, groovyism and convention rule sets:

Style guide rule CodeNarc rules (names as in the rule index)

1 No semicolons

UnnecessarySemicolon

2 Return keyword optional

UnnecessaryReturnKeyword; the opposite preference is ImplicitReturnStatement

3 Def and type

UnnecessaryDefInVariableDeclaration, UnnecessaryDefInMethodDeclaration, UnnecessaryDefInFieldDeclaration

4 Public by default

UnnecessaryPublicModifier

5 Omitting parentheses

UnnecessaryParenthesesForMethodCallWithClosure

6 Classes as first-class citizens

UnnecessaryDotClass

7 Getters and Setters

UnnecessaryGetter, UnnecessarySetter

10 Equals and ==

ExplicitCallToEqualsMethod

11 GStrings

UnnecessaryGString, GStringExpressionWithinString

12 Native syntax for data structures

ExplicitArrayListInstantiation, ExplicitHashMapInstantiation and the other Explicit...Instantiation rules

14 The power of switch

CouldBeSwitchStatement

17 Safe graph navigation

UnnecessarySafeNavigationOperator (flags a needless ?.)

19 Elvis operator

CouldBeElvis, TernaryCouldBeElvis

21 Optional typing advice

MethodReturnTypeRequired

Not every rule has a CodeNarc counterpart (the style of with and tap, or the choice of catch (any), are not checked), and some CodeNarc rules pull the other way from the guide: ImplicitReturnStatement asks for an explicit return, and CatchException flags catching Exception. Choose the rule set deliberately instead of enabling all of them.

A rule set is written in a small Groovy DSL, in a file such as codenarc-rules.groovy:

ruleset {
    description 'Rules that mirror the Groovy style guide'

    UnnecessarySemicolon
    UnnecessaryReturnKeyword
    UnnecessaryDefInMethodDeclaration
    UnnecessaryPublicModifier
    UnnecessaryGetter
    TernaryCouldBeElvis

    // or include a whole predefined set and remove what you disagree with
    // ruleset('rulesets/unnecessary.xml') { exclude 'UnnecessaryGString' }
}

CodeNarc can be run from the command line, an Ant task, Gradle’s codenarc plugin (tasks codenarcMain and codenarcTest, with the rule file set through configFile), Maven, and through its API. The program below uses the API to check a small snippet that breaks several rules. It ran under Groovy 6.0.0 with CodeNarc 4.0.0; CodeNarc 4.0.0 depends on Groovy 5.0.x, so the script excludes the org.apache.groovy artifacts from the grab to keep the running Groovy 6 modules (without the exclusion Groovy reports conflicting module versions). Check the CodeNarc release notes for the Groovy line your build uses before relying on that.

@GrabExclude(group = 'org.apache.groovy', module = '*')
@Grab('org.codenarc:CodeNarc:4.0.0')
import org.codenarc.analyzer.StringSourceAnalyzer
import org.codenarc.ruleset.GroovyDslRuleSet
import org.codenarc.ruleregistry.RuleRegistryInitializer

def source = '''class Plant {
    String name;
    public String label() { return name }
    def String kind() { 'herb' }
    String upper(flag) { flag ? flag : 'none' }
}
'''
new RuleRegistryInitializer().initializeRuleRegistry()    // lets the DSL find rules by name
def rules = new GroovyDslRuleSet('file:codenarc-rules.groovy')
def results = new StringSourceAnalyzer(source).analyze(rules)
results.violations.sort { it.lineNumber }.each {
    println "line ${it.lineNumber}: ${it.rule.name}"
}
line 2: UnnecessarySemicolon
line 3: UnnecessaryReturnKeyword
line 3: UnnecessaryPublicModifier
line 4: UnnecessaryDefInMethodDeclaration
line 5: TernaryCouldBeElvis

The script reads the rule file shown above, so it was run from the directory that contains it. A real build would point the tool at src/main/groovy and src/test/groovy, write an HTML report, and fail the build above a violation threshold per priority.

Where the book is outdated

The Manning book Groovy in Action (first edition, Manning 2007; publisher page in the references) was written for Groovy 1.0. The style rules themselves are unchanged, but the language moved on in every release since, through 2.x, 3.0, 4.0, 5.0 and 6.0. When you follow the book, check these against the current documentation:

  • The Parrot parser (Groovy 3.0, the only parser since 4.0) is stricter about some forms that used to compile, so code copied from older sources may need parentheses or a rewrite. The Elvis assignment ?= (rule 19) came with Groovy 3.0. var is described in Variables, Typing, def, var and val.

  • Records, traits and the AST transformations such as @Canonical, @TupleConstructor and @Immutable replace much hand-written bean code; see AST Transformations: Code Generation & Class Design.

  • The Java baseline is JDK 17 or later for Groovy 6.0, and static typing (@TypeChecked, @CompileStatic) is covered in its own page (rule 21).

The same topics in other languages of this site: Kotlin Lexical Structure & Style and Python Lexical Structure & Style. Common design problems and their idiomatic Groovy answers are in Design Patterns in Groovy.