What you'll learn

  • Properties and automatic getters/setters
  • Named-argument constructors
  • @Canonical, @ToString and other AST transformations
  • Traits for reusable behaviour

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
AnnotationGenerates
@ToStringa readable toString()
@EqualsAndHashCodeequals and hashCode
@TupleConstructora positional constructor
@Canonicalall three of the above
@Immutablean immutable value class
@Buildera builder API
@CompileStaticJava-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 @Canonical and @Immutable remove boilerplate.
  • Traits share state and behaviour across classes.

Example files