Groovy classes compile to normal Java classes — but you write a fraction of the code. No getters, no setters, no constructors to type by hand, and annotations that generate equals, hashCode and toString for you.
Properties
class Customer {
String name
String email
BigDecimal creditLimit = 5000
boolean canSpend(BigDecimal amount) { amount <= creditLimit }
}
A field declared without private/public becomes a property: Groovy generates getName() and setName() automatically. Access looks like field access but calls the getter/setter:
c.name // calls getName()
c.creditLimit = 1000 // calls setCreditLimit(1000)
Java code using your class sees normal getters and setters.
Named-argument constructors
def c = new Customer(name: 'Acme', email: 'ap@acme.test')
No constructor needed: Groovy creates the object and sets each named property.
AST transformations: code written for you
Annotations from groovy.transform generate code at compile time:
@Canonical
class Line {
String sku
int qty
BigDecimal price
BigDecimal getTotal() { qty * price }
}
def l = new Line('AX-200', 24, 8.50) // positional constructor
println l // Line(AX-200, 24, 8.50, 204.00)
l == new Line('AX-200', 24, 8.50) // true — value equality
| Annotation | Generates |
|---|---|
@ToString | a readable toString() |
@EqualsAndHashCode | equals and hashCode |
@TupleConstructor | a positional constructor |
@Canonical | all three of the above |
@Immutable | an immutable value class |
@Builder | a builder API |
@CompileStatic | Java-like static compilation and type checking |
Traits: reusable behaviour
A trait is like an interface that can also carry state and method implementations. A class can implement several:
trait Auditable {
List<String> history = []
void audit(String event) { history << event }
}
trait Validatable {
abstract List<String> errors()
boolean isValid() { errors().isEmpty() }
}
class Order implements Auditable, Validatable {
String id
List<Line> lines = []
List<String> errors() { lines ? [] : ['order has no lines'] }
}
Traits are a clean way to share capabilities — auditing, validation, retry behaviour — across unrelated classes without deep inheritance trees.
The full example
// classes.groovy — classes, properties, constructors, traits and @Canonical.
import groovy.transform.Canonical
import groovy.transform.ToString
// Properties: no getters/setters to write; Groovy generates them
class Customer {
String name
String email
BigDecimal creditLimit = 5000
boolean canSpend(BigDecimal amount) { amount <= creditLimit }
}
def c = new Customer(name: 'Acme', email: 'ap@acme.test') // named-argument constructor
println c.name // uses getName()
c.creditLimit = 1000 // uses setCreditLimit()
println c.canSpend(1200) // false
// @Canonical: equals, hashCode, toString and constructors for free
@Canonical
class Line {
String sku
int qty
BigDecimal price
BigDecimal getTotal() { qty * price }
}
def l1 = new Line('AX-200', 24, 8.50)
println l1 // Line(AX-200, 24, 8.50, 204.00)
println l1.total // 204.00
println l1 == new Line('AX-200', 24, 8.50) // true
// Traits: reusable behaviour you can mix into classes
trait Auditable {
List<String> history = []
void audit(String event) { history << "${java.time.LocalTime.now().withNano(0)} $event".toString() }
}
trait Validatable {
abstract List<String> errors()
boolean isValid() { errors().isEmpty() }
}
@ToString(includeNames = true)
class Order implements Auditable, Validatable {
String id
List<Line> lines = []
List<String> errors() { lines ? [] : ['order has no lines'] }
}
def o = new Order(id: 'PO-1')
println o.valid // false (isValid() as a property)
o.lines << l1
o.audit('line added')
println o.valid // true
println o.history.size() // 1
println o
Try it yourself
Create an @Immutable class Money with BigDecimal amount and String currency, and a method plus(Money other) that returns a new Money. Try changing amount on an instance and see what happens.
Show a solution
import groovy.transform.Immutable
@Immutable
class Money {
BigDecimal amount
String currency
Money plus(Money other) { new Money(amount + other.amount, currency) }
}
def m = new Money(10, 'USD') + new Money(5.5, 'USD') // + calls plus()
println m // Money(15.5, USD)
try { m.amount = 1 } catch (ReadOnlyPropertyException e) { println 'immutable!' }
Naming the method plus also enables the + operator — Groovy’s operator overloading.
Key takeaways
- Fields without modifiers are properties with generated getters and setters.
- Named-argument constructors come for free.
- AST transformations like
@Canonicaland@Immutableremove boilerplate. - Traits share state and behaviour across classes.