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 rules at a glance
| # | Rule | Covered in depth in |
|---|---|---|
1 |
||
2 |
||
3 |
||
4 |
||
5 |
||
6 |
||
7 |
||
8 |
Initializing beans with named parameters and the default constructor |
|
9 |
Using with() and tap() for repeated operations on the same bean |
|
10 |
||
11 |
||
12 |
||
13 |
||
14 |
||
15 |
||
16 |
||
17 |
||
18 |
||
19 |
||
20 |
||
21 |
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 |
|
2 Return keyword optional |
|
3 Def and type |
|
4 Public by default |
|
5 Omitting parentheses |
|
6 Classes as first-class citizens |
|
7 Getters and Setters |
|
10 Equals and == |
|
11 GStrings |
|
12 Native syntax for data structures |
|
14 The power of switch |
|
17 Safe graph navigation |
|
19 Elvis operator |
|
21 Optional typing advice |
|
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.varis described in Variables, Typing, def, var and val. -
Records, traits and the AST transformations such as
@Canonical,@TupleConstructorand@Immutablereplace 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.