Groovy constructors and named parameters are demonstrated with 10 tested examples. Learn map-based constructors, @TupleConstructor, default values, and builder pattern.
“A well-designed constructor is the first handshake between your class and the outside world. Make it count.”
Dierk König, Groovy in Action
Last Updated: March 2026 | Tested on: Groovy 4.x / 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes
If you have been working with Java, you know the drill: define a class, write a constructor with explicit parameters, assign each one to a field. Then write another constructor for the optional fields. Then maybe an overload for the three-argument case. Groovy throws most of that ceremony out the window.
Groovy constructors – covered in depth in the official Groovy constructors documentation – give you multiple ways to create objects, from auto-generated no-arg constructors and map-based named parameters to powerful AST annotations like @TupleConstructor, @MapConstructor, @Canonical, and @Immutable. The result is less boilerplate, more readable code, and fewer bugs from mixed-up argument order.
This post explains how each constructor style works, when to use which, and how to combine them effectively. We will walk through 10 tested examples covering every practical constructor pattern you will encounter in real Groovy projects.
From Grails domain objects and Gradle build scripts to Spock test fixtures, understanding Groovy constructors will make your code cleaner and more maintainable.
If you need a refresher on Groovy classes and inheritance, check our Groovy Classes and Inheritance guide first.
Table of Contents
How Constructors Work in Groovy
In Java, if you define a class without any constructor, the compiler generates a default no-arg constructor. Groovy does the same thing, but it goes further. Groovy also auto-generates a map-based constructor that accepts a Map of property names to values. This is the foundation of Groovy’s named parameters feature for object construction.
Here is the key difference from Java:
- No-arg constructor – automatically generated unless you define your own constructor
- Map-based constructor – automatically available for any class with properties. You pass property names as keys:
new Person(name: 'Alice', age: 30) - Custom constructors – you can define explicit constructors just like Java. But once you define any constructor, the auto-generated no-arg constructor disappears (unless you add it back)
- AST annotations – Groovy provides
@TupleConstructor,@MapConstructor,@Canonical, and@Immutableto generate constructors at compile time with minimal code
The map-based constructor works by calling the no-arg constructor first, then setting each property from the map. This means your class needs a no-arg constructor (either auto-generated or explicit) for the map-based syntax to work. If you define a custom constructor that takes parameters, you will lose the map-based constructor unless you also provide a no-arg constructor.
Here is a quick comparison to help you decide which approach to use:
Constructor Types at a Glance
Constructor Style | When to Use | Auto-Generated? --------------------------|--------------------------------------|---------------- No-arg constructor | Simple classes, set props after | Yes (default) Map-based (named params) | Readable construction, many fields | Yes (with no-arg) Custom constructor | Validation, computed fields | No (you write it) @TupleConstructor | Positional args without boilerplate | Yes (AST) @MapConstructor | Named params with custom constructors| Yes (AST) @Canonical | Full-featured mutable value objects | Yes (AST) @Immutable | Immutable objects with copyWith | Yes (AST)
One important detail: Groovy properties (fields declared without an access modifier) automatically get generated getters and setters. This is what makes the map-based constructor work – it calls each setter behind the scenes. If you use private or protected fields instead of properties, they will not be accessible through the map-based constructor.
Another thing to keep in mind: the order of property setting in the map-based constructor is not guaranteed. Do not rely on one property being set before another. If property B depends on property A being set first, use a custom constructor or @TupleConstructor instead, where the order is deterministic.
Groovy also supports coercion with the as keyword and list-based construction. For example, ['Alice', 30] as Person will call the tuple constructor of Person with those positional arguments. This is particularly useful in testing and DSLs, but we will focus on the more common constructor patterns in this guide.
Here is all of this in action with 10 practical examples.
10 Tested Examples of Groovy Constructors
Every example below has been tested on Groovy 4.x and 5.x with Java 17+. We will build up from the simplest case to the most advanced patterns.
Example 1: Default No-Arg Constructor (Auto-Generated)
What we’re doing: Defining a simple class with properties and relying on Groovy’s auto-generated no-arg constructor. Properties are set after construction using standard setters.
Example 1: Default No-Arg Constructor
class Book {
String title
String author
int pages
}
// Groovy auto-generates a no-arg constructor
def book = new Book()
book.title = 'Groovy in Action'
book.author = 'Dierk Koenig'
book.pages = 912
println "Title: ${book.title}"
println "Author: ${book.author}"
println "Pages: ${book.pages}"
Output
Title: Groovy in Action Author: Dierk Koenig Pages: 912
What happened here: We defined no constructor at all. Groovy automatically generated a no-arg constructor, so new Book() works out of the box. We then set each property individually using Groovy’s auto-generated setters. This is the simplest pattern, but it means your object is mutable and can exist in a partially initialized state.
This pattern is fine for scripts and quick prototypes, but for production code you will usually want either named parameters (Example 2) or AST annotations (Examples 4-5, 10) to ensure your objects are fully initialized at construction time. A partially initialized object is a bug waiting to happen – imagine calling book.title.toUpperCase() before setting the title. You would get a NullPointerException.
Example 2: Map-Based Constructor (Named Parameters)
What we’re doing: Using Groovy’s map-based constructor to pass property values as named parameters. This is arguably the most “Groovy” way to create objects.
Example 2: Map-Based Constructor (Named Parameters)
class Employee {
String name
String department
double salary
String email
}
// Named parameters - order does not matter
def emp = new Employee(
department: 'Engineering',
name: 'Alice Johnson',
salary: 95000.00,
email: 'alice@example.com'
)
println "${emp.name} works in ${emp.department}"
println "Salary: \$${emp.salary}"
println "Email: ${emp.email}"
// You can also skip optional properties
def emp2 = new Employee(name: 'Bob', department: 'Marketing')
println "\n${emp2.name} works in ${emp2.department}"
println "Salary: \$${emp2.salary}" // default value: 0.0
println "Email: ${emp2.email}" // default value: null
Output
Alice Johnson works in Engineering Salary: $95000.0 Email: alice@example.com Bob works in Marketing Salary: $0.0 Email: null
What happened here: Groovy’s map-based constructor lets you pass properties in any order using key: value syntax. Under the hood, Groovy calls the no-arg constructor, then invokes each property’s setter. Properties you skip get their type’s default value (null for objects, 0 for numbers, false for booleans). This is extremely useful when your class has many fields and you only need to set a few.
Named parameters are one of the biggest quality-of-life improvements Groovy offers over Java. Consider a class with five String fields – in Java, you would write new Config("a", "b", "c", "d", "e") and hope you got the order right. In Groovy, each value is labeled, making the code self-documenting and resistant to argument-order bugs.
Watch out: If you pass a key that does not match any property, Groovy will throw a MissingPropertyException at runtime. There is no compile-time check for this unless you use @CompileStatic.
Example 3: Custom Constructors
What we’re doing: Defining explicit constructors, just like in Java. This gives you full control over initialization logic, but removes the auto-generated no-arg constructor.
Example 3: Custom Constructors
class Product {
String name
double price
String category
// Custom constructor
Product(String name, double price) {
this.name = name
this.price = price
this.category = 'General'
}
// Overloaded constructor with all fields
Product(String name, double price, String category) {
this.name = name
this.price = price
this.category = category
}
String toString() {
"Product(name: $name, price: \$$price, category: $category)"
}
}
def p1 = new Product('Laptop', 999.99)
def p2 = new Product('Mouse', 29.99, 'Accessories')
println p1
println p2
// This would FAIL - no no-arg constructor exists anymore:
// def p3 = new Product() // ERROR: No matching constructor
// This would also FAIL - no map-based constructor:
// def p4 = new Product(name: 'Keyboard', price: 49.99) // ERROR
Output
Product(name: Laptop, price: $999.99, category: General) Product(name: Mouse, price: $29.99, category: Accessories)
What happened here: Once you define any custom constructor, Groovy stops generating the default no-arg constructor. This means the map-based constructor also stops working (because it depends on the no-arg constructor internally). If you want both custom constructors and the map-based syntax, you need to explicitly add a no-arg constructor. This is a common gotcha for developers coming from Java.
Tip: If you want to keep the map-based constructor while also having custom constructors, add an explicit no-arg constructor:
Restoring Map-Based Constructor
class Product2 {
String name
double price
String category
// Explicit no-arg constructor - restores map-based syntax
Product2() {}
Product2(String name, double price) {
this.name = name
this.price = price
this.category = 'General'
}
}
// Now both work:
def p1 = new Product2(name: 'Keyboard', price: 49.99)
def p2 = new Product2('Mouse', 29.99)
println "p1: ${p1.name} - \$${p1.price}"
println "p2: ${p2.name} - \$${p2.price} (${p2.category})"
Output
p1: Keyboard - $49.99 p2: Mouse - $29.99 (General)
Example 4: @TupleConstructor Annotation
What we’re doing: Using the @TupleConstructor AST transformation to auto-generate positional constructors at compile time. This saves you from writing boilerplate constructor code. For more on Groovy’s AST transformations, see our AST Transformations guide.
Example 4: @TupleConstructor
import groovy.transform.TupleConstructor
@TupleConstructor
class Address {
String street
String city
String state
String zipCode
}
// Positional constructor - pass values in field declaration order
def addr1 = new Address('123 Main St', 'Springfield', 'IL', '62701')
println addr1.street // 123 Main St
println addr1.city // Springfield
// You can skip trailing arguments - they get default values
def addr2 = new Address('456 Oak Ave', 'Chicago')
println addr2.state // null
println addr2.zipCode // null
// No-arg constructor is also available
def addr3 = new Address()
println addr3.street // null
// Map-based constructor still works too
def addr4 = new Address(city: 'Austin', state: 'TX')
println "${addr4.city}, ${addr4.state}"
Output
123 Main St Springfield null null null Austin, TX
What happened here: @TupleConstructor generates a constructor that accepts arguments in the same order as the fields are declared. It also preserves the no-arg constructor, so map-based construction continues to work. You can pass fewer arguments than there are fields – the remaining fields get their default values. This is one of the most commonly used Groovy AST annotations.
@TupleConstructor also supports several useful options:
includes/excludes– control which properties appear in the constructorincludeFields– include private fields (not just properties)includeSuperProperties– include properties from the parent classforce– generate the constructor even if one already existsdefaults– whether to generate the permutation constructors (default:true)
For example, @TupleConstructor(includes = ['street', 'city']) would generate a constructor that only accepts street and city, ignoring state and zipCode.
Example 5: @MapConstructor Annotation
What we’re doing: Using @MapConstructor to explicitly generate a map-based constructor. This is useful when you want named parameters combined with other constructor styles or when you have custom constructors that would otherwise disable the map-based syntax.
Example 5: @MapConstructor
import groovy.transform.MapConstructor
@MapConstructor
class ServerConfig {
String host
int port
boolean ssl
String protocol
String toString() {
"${protocol ?: 'http'}://${host}:${port} (SSL: ${ssl})"
}
}
// Named parameters - clear and self-documenting
def config = new ServerConfig(
host: 'api.example.com',
port: 8443,
ssl: true,
protocol: 'https'
)
println config
// Partial construction - only set what you need
def devConfig = new ServerConfig(
host: 'localhost',
port: 8080
)
println devConfig
Output
https://api.example.com:8443 (SSL: true) http://localhost:8080 (SSL: false)
What happened here: @MapConstructor generates a constructor that accepts a Map, just like Groovy’s built-in map-based constructor, but as an explicit AST transformation. The advantage is that it works reliably even when you have other custom constructors defined. Unset properties get their default values: null for objects, 0 for numbers, false for booleans.
When to use @MapConstructor vs. the built-in map constructor: Use @MapConstructor when you also have custom constructors that would otherwise disable the built-in map-based syntax, or when you want to use its pre and post closure options for running code before or after property assignment. For simple classes with no custom constructors, the built-in map constructor is sufficient.
Example 6: Constructor with Default Parameter Values
What we’re doing: Using Groovy’s default parameter values in constructors. This is a language feature (not an annotation) that generates multiple constructor overloads at compile time.
Example 6: Default Parameter Values
class DatabaseConnection {
String host
int port
String database
String username
int maxConnections
DatabaseConnection(String host, int port = 5432,
String database = 'mydb',
String username = 'admin',
int maxConnections = 10) {
this.host = host
this.port = port
this.database = database
this.username = username
this.maxConnections = maxConnections
}
String toString() {
"jdbc:postgresql://${host}:${port}/${database}" +
" (user: ${username}, pool: ${maxConnections})"
}
}
// All defaults
def db1 = new DatabaseConnection('localhost')
println db1
// Override some defaults
def db2 = new DatabaseConnection('prod-server', 5433, 'production')
println db2
// Override all
def db3 = new DatabaseConnection('replica', 5434, 'analytics', 'reader', 5)
println db3
Output
jdbc:postgresql://localhost:5432/mydb (user: admin, pool: 10) jdbc:postgresql://prod-server:5433/production (user: admin, pool: 10) jdbc:postgresql://replica:5434/analytics (user: reader, pool: 5)
What happened here: Groovy generates multiple constructor overloads from a single constructor definition. A constructor with n default parameters generates n + 1 constructors. The defaults apply from right to left – you cannot skip a middle parameter and set a later one (for that, use named parameters instead). This pattern is great when you have a clear “most common” set of defaults.
How the overloads work internally: For our DatabaseConnection constructor with 4 default parameters, Groovy generates 5 constructors at compile time:
DatabaseConnection(String host)– uses all defaultsDatabaseConnection(String host, int port)– overrides portDatabaseConnection(String host, int port, String database)– overrides port and databaseDatabaseConnection(String host, int port, String database, String username)– overrides three fieldsDatabaseConnection(String host, int port, String database, String username, int maxConnections)– all explicit
This is a compile-time feature, not a runtime one. The generated bytecode contains five separate constructor methods. If you need to skip a middle parameter (e.g., set maxConnections but keep the default database), you cannot do it with default parameters alone – use named parameters or the builder pattern instead.
Example 7: Copy Constructors
What we’re doing: Implementing a copy constructor that creates a new object from an existing one. This is useful when you need a modified copy of an object without mutating the original.
Example 7: Copy Constructor
import groovy.transform.TupleConstructor
@TupleConstructor
class UserProfile {
String username
String email
String role
boolean active
// Copy constructor
UserProfile(UserProfile other) {
this.username = other.username
this.email = other.email
this.role = other.role
this.active = other.active
}
// Copy with modifications using a map
UserProfile copyWith(Map changes) {
def copy = new UserProfile(this)
changes.each { key, value ->
copy."$key" = value
}
return copy
}
String toString() {
"UserProfile(${username}, ${email}, ${role}, active: ${active})"
}
}
def original = new UserProfile('alice', 'alice@example.com', 'USER', true)
println "Original: $original"
// Exact copy
def copy = new UserProfile(original)
println "Copy: $copy"
// Modified copy - promote to admin
def admin = original.copyWith(role: 'ADMIN', email: 'alice-admin@example.com')
println "Admin: $admin"
// Original is unchanged
println "Original: $original"
Output
Original: UserProfile(alice, alice@example.com, USER, active: true) Copy: UserProfile(alice, alice@example.com, USER, active: true) Admin: UserProfile(alice, alice-admin@example.com, ADMIN, active: true) Original: UserProfile(alice, alice@example.com, USER, active: true)
What happened here: The copy constructor takes another instance of the same class and copies all its fields. The copyWith method goes a step further – it creates a copy, then applies changes from a map. This pattern is common when you want semi-immutable objects: create them once, then produce modified copies instead of mutating the original. For truly immutable objects, see the @Immutable annotation in Example 10.
Important: The copy constructor here performs a shallow copy. If your class has fields that are mutable objects (like a List or another custom class), the copy will share references to those same objects. Modifying a list field on the copy will also affect the original. For deep copies, you need to explicitly clone or recreate each mutable field. The @Immutable annotation handles this automatically by performing defensive copies of mutable fields like lists and maps.
Example 8: Constructor Chaining with this()
What we’re doing: Using this() to chain constructors within the same class. This eliminates duplicate initialization logic when you have multiple constructors.
Example 8: Constructor Chaining
class HttpRequest {
String method
String url
Map<String, String> headers
String body
int timeout
// Full constructor - all parameters
HttpRequest(String method, String url,
Map<String, String> headers,
String body, int timeout) {
this.method = method.toUpperCase()
this.url = url
this.headers = headers ?: [:]
this.body = body
this.timeout = timeout
}
// Chain: method + url + headers
HttpRequest(String method, String url,
Map<String, String> headers) {
this(method, url, headers, null, 30000)
}
// Chain: method + url only
HttpRequest(String method, String url) {
this(method, url, [:], null, 30000)
}
// Chain: GET shortcut
HttpRequest(String url) {
this('GET', url)
}
String toString() {
"${method} ${url} (timeout: ${timeout}ms, " +
"headers: ${headers.size()}, body: ${body ? 'yes' : 'no'})"
}
}
def get = new HttpRequest('https://api.example.com/users')
println get
def post = new HttpRequest('POST', 'https://api.example.com/users',
['Content-Type': 'application/json'], '{"name":"Alice"}', 5000)
println post
def del = new HttpRequest('DELETE', 'https://api.example.com/users/1')
println del
Output
GET https://api.example.com/users (timeout: 30000ms, headers: 0, body: no) POST https://api.example.com/users (timeout: 5000ms, headers: 1, body: yes) DELETE https://api.example.com/users/1 (timeout: 30000ms, headers: 0, body: no)
What happened here: Each simpler constructor delegates to a more complete one using this(). The full constructor at the top handles all the initialization logic – normalization (toUpperCase()), null checking (headers ?: [:]), and assignment. The simpler constructors just supply default values. This avoids duplicating logic and ensures every construction path goes through the same validation. Note that this() must be the first statement in the constructor body, just like in Java.
Constructor chaining vs. default parameters: You might be wondering why we didn’t just use default parameter values here (like in Example 6). Both approaches are valid, but constructor chaining gives you more control. With chaining, each constructor can apply different logic before delegating. Default parameters simply generate overloads that all call the same full constructor. Choose constructor chaining when you need different initialization logic per constructor; choose default parameters when you just need to provide sensible defaults.
Chaining to super(): You can also chain to a parent class constructor using super() instead of this(). This is essential when your class extends another class that has required constructor arguments. For inheritance details, see our Groovy Classes and Inheritance guide.
Example 9: Constructor with Validation
What we’re doing: Adding validation logic inside constructors to enforce invariants. An object should never exist in an invalid state.
Example 9: Constructor with Validation
class EmailAddress {
final String value
EmailAddress(String email) {
if (!email || email.trim().isEmpty()) {
throw new IllegalArgumentException(
'Email address cannot be null or empty')
}
if (!email.contains('@') || !email.contains('.')) {
throw new IllegalArgumentException(
"Invalid email format: '${email}'")
}
this.value = email.trim().toLowerCase()
}
String toString() { value }
}
class BankAccount {
final String accountNumber
final String owner
double balance
BankAccount(String accountNumber, String owner,
double initialBalance = 0.0) {
if (!accountNumber || accountNumber.length() < 8) {
throw new IllegalArgumentException(
'Account number must be at least 8 characters')
}
if (!owner || owner.trim().isEmpty()) {
throw new IllegalArgumentException(
'Owner name is required')
}
if (initialBalance < 0) {
throw new IllegalArgumentException(
"Initial balance cannot be negative: ${initialBalance}")
}
this.accountNumber = accountNumber
this.owner = owner.trim()
this.balance = initialBalance
}
String toString() {
"Account ${accountNumber} (${owner}): \$${balance}"
}
}
// Valid construction
def email = new EmailAddress('Alice@Example.COM')
println "Email: $email" // normalized to lowercase
def account = new BankAccount('12345678', 'Alice Johnson', 1000.00)
println account
// Validation failures
try {
new EmailAddress('not-an-email')
} catch (IllegalArgumentException e) {
println "Error: ${e.message}"
}
try {
new BankAccount('123', 'Bob')
} catch (IllegalArgumentException e) {
println "Error: ${e.message}"
}
try {
new BankAccount('12345678', 'Carol', -500.0)
} catch (IllegalArgumentException e) {
println "Error: ${e.message}"
}
Output
Email: alice@example.com Account 12345678 (Alice Johnson): $1000.0 Error: Invalid email format: 'not-an-email' Error: Account number must be at least 8 characters Error: Initial balance cannot be negative: -500.0
What happened here: Constructors are the last line of defense for your class invariants. If the data is invalid, throw an exception immediately – do not allow an invalid object to exist. The EmailAddress class normalizes input (trimming and lowercasing) in addition to validating. The BankAccount class validates multiple fields. Using final fields ensures they cannot be changed after construction, creating a partially immutable design.
Validation strategy tips:
- Fail fast – validate and throw at the top of the constructor, before any assignment
- Use
IllegalArgumentException– this is the Java/Groovy convention for invalid constructor arguments - Include the bad value in the message –
"Invalid email: '${email}'"is much more helpful than"Invalid email" - Normalize inputs – trim whitespace, lowercase emails, strip leading zeros. The constructor is the right place for this
- Mark validated fields as
final– once validated and assigned, they should not change. This prevents the object from being put back into an invalid state later
If you find yourself writing a lot of validation logic, consider extracting it into a static validate() method or using Groovy’s assert keyword for quick checks: assert email?.contains('@') : "Email must contain @".
Example 10: @Canonical and @Immutable Constructors
What we’re doing: Using @Canonical and @Immutable – two powerful AST transformations that generate constructors along with equals(), hashCode(), and toString(). For more details, see our Groovy @Immutable guide.
Example 10: @Canonical and @Immutable
import groovy.transform.Canonical
import groovy.transform.Immutable
// @Canonical = @ToString + @EqualsAndHashCode + @TupleConstructor
@Canonical
class Point {
double x
double y
}
// Positional constructor (from @TupleConstructor)
def p1 = new Point(3.0, 4.0)
println p1 // auto-generated toString
// Map-based constructor also works
def p2 = new Point(x: 1.0, y: 2.0)
println p2
// equals() is generated - compares by value
def p3 = new Point(3.0, 4.0)
println "p1 == p3: ${p1 == p3}" // true (same values)
println "p1 == p2: ${p1 == p2}" // false (different values)
println '---'
// @Immutable - all fields become final, no setters
@Immutable
class Color {
int red
int green
int blue
}
def red = new Color(255, 0, 0)
println red
def blue = new Color(red: 0, green: 0, blue: 255)
println blue
// copyWith - creates a modified copy (available with @Immutable)
def purple = red.copyWith(blue: 200)
println "Purple: $purple"
// Trying to modify throws an error
try {
red.red = 100 // ReadOnlyPropertyException
} catch (ReadOnlyPropertyException e) {
println "Cannot modify: ${e.message}"
}
Output
Point(3.0, 4.0) Point(1.0, 2.0) p1 == p3: true p1 == p2: false --- Color(255, 0, 0) Color(0, 0, 255) Purple: Color(255, 0, 200) Cannot modify: Cannot set readonly property: red for class: Color
What happened here: @Canonical is a convenience annotation that combines @ToString, @EqualsAndHashCode, and @TupleConstructor. It gives you a fully-featured class with minimal boilerplate. @Immutable goes further: it makes all fields final, removes setters, generates both positional and map-based constructors, and adds a copyWith method for creating modified copies. Use @Canonical for mutable value objects and @Immutable when you need true immutability. For more on the builder pattern approach to constructing complex objects, check our Groovy Builder Pattern guide.
@Immutable restrictions: All fields must be of known immutable types (primitives, String, Date, other @Immutable classes) or collections (which get defensively copied). If you have a field of a mutable type that Groovy does not recognize as immutable, you will get a compile error. You can work around this by adding the type to knownImmutables or knownImmutableClasses in the annotation options.
Here is a quick summary of what each annotation generates:
Annotation Comparison
Feature | @Canonical | @Immutable ---------------------|------------|------------ Tuple constructor | Yes | Yes Map constructor | Yes* | Yes toString() | Yes | Yes equals() / hashCode()| Yes | Yes Fields are final | No | Yes Setters removed | No | Yes copyWith() | No | Yes Defensive copying | No | Yes * Map constructor works via preserved no-arg constructor
Common Pitfalls
Before we get to best practices, let’s address the mistakes that trip up most developers when working with Groovy constructors.
Pitfall 1: Typo in Named Parameter Keys
Named parameters are not checked at compile time (unless you use @CompileStatic). A typo in a key name silently creates a MissingPropertyException at runtime:
Typo in Named Parameter
class User {
String name
String email
}
// Typo: 'emial' instead of 'email'
try {
def user = new User(name: 'Alice', emial: 'alice@example.com')
} catch (MissingPropertyException e) {
println "Error: ${e.message}"
}
Output
Error: No such property: emial for class: User
This only fails at runtime, which can be problematic. Using @CompileStatic on your calling code will catch these errors at compile time.
Pitfall 2: Constructor Conflicts with @TupleConstructor
If you add a custom constructor to a class annotated with @TupleConstructor, the annotation will not overwrite it. However, the generated constructors might conflict with your custom ones if they have the same signature. Use the force option to override, or carefully plan your parameter types to avoid ambiguity.
Pitfall 3: Mutable Fields in @Immutable Classes
If your @Immutable class has a field of a type that Groovy does not recognize as immutable, compilation will fail. For example, a StringBuilder field will cause an error. Either use only known immutable types or declare the type in knownImmutables.
Best Practices
After working through all 10 examples, here are the patterns and practices that will serve you well in production Groovy code:
- Prefer named parameters for classes with more than 3 fields. Positional constructors become error-prone when you have multiple parameters of the same type.
new User('Alice', 'alice@example.com', 'admin', 'Engineering')is harder to read thannew User(name: 'Alice', email: 'alice@example.com', role: 'admin', department: 'Engineering'). - Use
@TupleConstructorfor simple data classes. It generates both positional and no-arg constructors with zero boilerplate. Combine it with@ToStringand@EqualsAndHashCode(or just use@Canonical). - Use
@Immutablefor value objects. If an object should not change after creation – things like money amounts, coordinates, configuration snapshots – mark it@Immutable. ThecopyWithmethod handles the “create a modified copy” pattern elegantly. - Always validate in constructors. Do not rely on callers to pass valid data. Check for null, empty strings, negative numbers, and invalid ranges. Throw
IllegalArgumentExceptionwith a clear message. An object should never exist in an invalid state. - Be careful with custom constructors and map-based syntax. Remember that defining any custom constructor removes the auto-generated no-arg constructor, which breaks the map-based syntax. If you need both, either add an explicit no-arg constructor or use
@MapConstructor. - Use constructor chaining to avoid duplication. If you have multiple constructors, delegate to the most complete one using
this(). Keep initialization logic in one place. - Consider the builder pattern for complex objects. When a class has many optional parameters, validation rules, and computed fields, a builder pattern may be more readable than a constructor with 15 parameters.
- Document which constructor styles your class supports. Groovy’s flexibility means a class might support positional, named, and no-arg construction simultaneously. Make it clear to other developers which style is the intended primary API.
Conclusion
Groovy gives you a rich toolkit for object construction that goes far beyond what Java offers out of the box. From the auto-generated map-based constructor (named parameters) to powerful AST annotations like @TupleConstructor, @MapConstructor, @Canonical, and @Immutable, you can choose the right approach for each situation.
The main points from this post:
- Groovy auto-generates a no-arg constructor and supports map-based named parameters by default
- Defining custom constructors removes the auto-generated no-arg constructor – a common gotcha
@TupleConstructorgenerates positional constructors;@MapConstructorgenerates map-based constructors@Canonicalcombines@ToString,@EqualsAndHashCode, and@TupleConstructorin one annotation@Immutablecreates truly immutable objects withcopyWithsupport- Always validate inputs in constructors – never allow invalid objects to exist
- Use constructor chaining (
this()) to avoid duplicating initialization logic
The constructor style you choose depends on your specific requirements:
- Quick scripts and prototypes – use the default no-arg constructor with map-based named parameters
- Data classes – use
@Canonicalor@TupleConstructorfor automatic boilerplate generation - Value objects and config – use
@Immutablefor safety andcopyWithsupport - Complex initialization logic – use custom constructors with validation and constructor chaining
- Many optional parameters – use
@MapConstructoror the builder pattern
For related topics, explore Groovy Classes and Inheritance for the fundamentals, Groovy @Immutable for deep immutability patterns, and Groovy Builder Pattern for constructing complex object graphs.
Up next: Groovy XmlSlurper – Parse XML the Easy Way
Frequently Asked Questions
What is the difference between @TupleConstructor and @MapConstructor in Groovy?
@TupleConstructor generates a positional constructor where arguments are passed in the same order as the fields are declared (e.g., new Person('Alice', 30)). @MapConstructor generates a constructor that accepts a Map, enabling named parameter syntax (e.g., new Person(name: 'Alice', age: 30)). You can use both on the same class if you want both construction styles.
Why does my map-based constructor stop working when I add a custom constructor?
Groovy’s map-based constructor relies on the auto-generated no-arg constructor. When you define any custom constructor, Groovy stops generating the no-arg constructor, which breaks the map-based syntax. To fix this, either add an explicit no-arg constructor (MyClass() {}), use the @MapConstructor annotation, or use @TupleConstructor which preserves the no-arg constructor.
When should I use @Canonical vs @Immutable?
Use @Canonical when you want a mutable class with auto-generated toString(), equals(), hashCode(), and a tuple constructor. Use @Immutable when you need a truly immutable object – all fields are made final, setters are removed, and a copyWith method is added. Use @Immutable for value objects like money, coordinates, and configuration snapshots.
Can I combine named parameters with positional parameters in Groovy constructors?
Yes, but with a specific rule: the named parameters (map entries) are collected into a Map as the first argument. For example, if your constructor is MyClass(Map args, String name), you can call it as new MyClass(name: 'Alice', key: 'value', 'positionalArg'). The map entries go into args and the positional value goes to name. This is an advanced pattern and can be confusing, so use it sparingly.
How do Groovy constructors compare to Java records?
Java records (Java 16+) are immutable data carriers with auto-generated constructors, equals(), hashCode(), and toString(). They are similar to Groovy’s @Immutable annotation but with some differences: records cannot extend other classes, they support compact constructors for validation, and they use positional construction only (no built-in named parameter support). Groovy’s @Immutable offers more flexibility with copyWith, defensive copying of mutable fields, and map-based construction.
Related Posts
Previous in Series: Groovy Interfaces
Next in Series: Groovy XmlSlurper – Parse XML the Easy Way
Related Topics You Might Like:
This post is part of the Groovy Cookbook series on TechnoScripts.com

No comment