The Groovy def keyword is covered here with 12 practical examples. Learn dynamic typing, type inference, and when to use def vs explicit types. Tested on Groovy 5.0.6.
“The best code is the code you don’t have to write. Groovy’s def keyword is proof of that.”
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Beginner | Reading Time: 14 minutes
If you’ve looked at any Groovy code, you’ve probably seen the Groovy def keyword everywhere. It’s one of the first things that stands out when coming from Java – instead of declaring String name = "John", Groovy developers just write def name = "John". But what does def actually do? And when should you use it versus specifying the type explicitly?
This post explains exactly what the def keyword does, how Groovy dynamic typing works under the hood, and when it makes sense to use def versus explicit type declarations. We’ll look at 12 practical examples covering variables, methods, closures, and some gotchas that trip up even experienced developers.
If you haven’t set up Groovy yet, check out our Groovy Hello World guide first to get up and running.
Table of Contents
What is the Groovy Def Keyword?
The def keyword in Groovy is a way to declare a variable, method, or property without specifying its type. It tells Groovy: “I don’t care about the type right now – figure it out at runtime.”
Behind the scenes, def is essentially an alias for Object. When you write def x = 42, the compiled bytecode treats x as type Object, but Groovy’s runtime knows it’s an Integer and handles method dispatch accordingly.
The diagram below shows the journey of a def variable from source code to a method call: it compiles to Object, keeps its real runtime type, and then routes through the Meta-Object Protocol for dynamic dispatch, versus the direct JVM call you get with @CompileStatic:
According to the official Groovy documentation, “def is a replacement for a type name. It can be used to indicate that you don’t care about the type of a variable.”
Key Points:
defmakes a variable dynamically typed – the type is resolved at runtime- Under the hood,
defcompiles toObject - A variable declared with
defcan hold any type and change types at any time - You can still use explicit types like
String,int,List– Groovy is optionally typed - In scripts, you can even skip
defentirely – bare assignments create binding variables
Why Use Def in Groovy?
The main appeal of def is simplicity. When the type is obvious from the right-hand side of an assignment, declaring it again on the left feels redundant. Compare these:
Java-Style Explicit Types
// Java-style explicit types String name = "Alice" Integer age = 30 List<String> cities = ["London", "Tokyo", "Paris"] Map<String, Integer> scores = [math: 95, english: 88] println name println age println cities println scores
Output
Alice 30 [London, Tokyo, Paris] [math:95, english:88]
Groovy Def Style
// Groovy def - cleaner, same result def name = "Alice" def age = 30 def cities = ["London", "Tokyo", "Paris"] def scores = [math: 95, english: 88] println name println age println cities println scores
Output
Alice 30 [London, Tokyo, Paris] [math:95, english:88]
Both versions do the exact same thing and print the exact same output. The def version is shorter and easier to read – you see what the variable is from the value, not the type declaration. Note the two versions are separate scripts here – in a single script you cannot declare the same variable name twice, even when switching from an explicit type to def.
Other benefits of using def:
- Flexibility: Variables can change types if needed (though this should be rare)
- Less boilerplate: No need to import or repeat complex generic types
- Faster prototyping: Focus on logic, not type declarations
- Duck typing: “If it walks like a duck and quacks like a duck, it’s a duck” – Groovy cares about what an object can do, not what type it is
When to Use Def vs Explicit Types
This is where it gets practical. Here’s a simple guide:
| Scenario | Use | Why |
|---|---|---|
| Local variables with obvious values | def | Type is clear from the right side |
| Method parameters in APIs | Explicit type | Documents what callers should pass |
| Method return types in APIs | Explicit type | Documents what callers get back |
| Scripts and quick prototypes | def | Speed over formality |
| Performance-critical code | Explicit type + @CompileStatic | Enables compile-time optimization |
| Domain class properties | Explicit type | GORM needs types for mapping |
The rule of thumb: use def for local variables and private internals. Use explicit types for public APIs, method signatures, and anything other developers will call.
How Def Works Under the Hood
When you use def, Groovy doesn’t just ignore types. It uses a mechanism called dynamic dispatch through the Meta-Object Protocol (MOP). Here’s the process:
- Compilation:
defvariables compile to typeObjectin the bytecode - Assignment: The actual value gets stored with its real type (Integer, String, etc.)
- Method calls: Groovy’s runtime looks up the actual type and dispatches the right method
- Type checking: Happens at runtime, not compile time – so type errors show up when you run the code
Def Under the Hood
def x = 42 println x.getClass().name // java.lang.Integer x = "now I'm a String" println x.getClass().name // java.lang.String x = [1, 2, 3] println x.getClass().name // java.util.ArrayList
Output
java.lang.Integer java.lang.String java.util.ArrayList
The variable x started as an Integer, became a String, then became a List – all in the same variable. The type declaration is Object, but the actual runtime type changes with each assignment.
Syntax and Basic Usage
Variable Declaration
Def Syntax
// Variable declaration with def
def variableName = value
// Method declaration with def return type
def methodName(parameters) {
// body
}
// Closure with def
def closureName = { params -> body }
Def vs No Def in Scripts
There’s a subtle but important difference in Groovy scripts:
Def vs No Def in Scripts
// With def - local variable, scoped to the script
def localVar = "I'm local"
// Without def - binding variable, accessible from methods
globalVar = "I'm in the binding"
def showVars() {
// println localVar // ERROR - can't access local var
println globalVar // OK - binding variable
}
showVars()
Output
I'm in the binding
In scripts, def creates a local variable. Without def, the variable goes into the script’s Binding object, making it accessible from methods defined in the same script.
12 Practical Def Keyword Examples
Example 1: Basic Variable Declaration
What we’re doing: Declaring variables of different types with def.
Example 1: Basic Variables
def name = "Alice"
def age = 28
def salary = 75000.50
def active = true
println "Name: ${name} (${name.getClass().simpleName})"
println "Age: ${age} (${age.getClass().simpleName})"
println "Salary: ${salary} (${salary.getClass().simpleName})"
println "Active: ${active} (${active.getClass().simpleName})"
Output
Name: Alice (String) Age: 28 (Integer) Salary: 75000.50 (BigDecimal) Active: true (Boolean)
What happened here: Even though we used def for all variables, Groovy assigned the correct type based on the value. Notice how 75000.50 became a BigDecimal, not a Double – Groovy uses BigDecimal by default for decimal literals to avoid floating-point precision issues.
Example 2: Type Reassignment
What we’re doing: Showing how a def variable can change types.
Example 2: Type Reassignment
def value = 100
println "Start: ${value} → ${value.getClass().simpleName}"
value = "hundred"
println "After: ${value} → ${value.getClass().simpleName}"
value = [1, 2, 3]
println "Final: ${value} → ${value.getClass().simpleName}"
Output
Start: 100 → Integer After: hundred → String Final: [1, 2, 3] → ArrayList
What happened here: The same variable changed from Integer to String to ArrayList. This is possible because def compiles to Object, which can hold anything. With an explicit type like int value = 100, the reassignment to a String would fail at compile time.
Example 3: Def with Collections
What we’re doing: Using def with lists and maps.
Example 3: Collections
def fruits = ["apple", "banana", "cherry"]
def person = [name: "Bob", age: 25, city: "London"]
println "Fruits: ${fruits}"
println "First fruit: ${fruits[0]}"
println "Person: ${person}"
println "Name: ${person.name}"
Output
Fruits: [apple, banana, cherry] First fruit: apple Person: [name:Bob, age:25, city:London] Name: Bob
What happened here: def works naturally with Groovy’s list and map literals. You get full access to all collection methods – each(), collect(), findAll() – without ever specifying the type.
Example 4: Def in Method Return Types
What we’re doing: Using def as a method return type.
Example 4: Method Return Type
def getGreeting(String name) {
return "Hello, ${name}!"
}
def getAge() {
return 30
}
def getConfig() {
return [debug: true, port: 8080]
}
println getGreeting("Alice")
println "Age: ${getAge()}"
println "Config: ${getConfig()}"
Output
Hello, Alice! Age: 30 Config: [debug:true, port:8080]
What happened here: Each method returns a different type – String, Integer, and Map – but they all use def as the return type. Groovy figures out the actual type from what’s returned. In scripts and internal code, this is perfectly fine. For public APIs, consider using explicit return types for documentation.
Example 5: Def in For-Each Loops
What we’re doing: Using def in loop variable declarations.
Example 5: Loop Variables
def numbers = [10, 20, 30, 40, 50]
// Using def in a for loop
for (def num in numbers) {
print "${num} "
}
println()
// More idiomatic - just use the type directly
for (int num in numbers) {
print "${num * 2} "
}
println()
// Most idiomatic - use each closure
numbers.each { num ->
print "${num + 1} "
}
println()
Output
10 20 30 40 50 20 40 60 80 100 11 21 31 41 51
What happened here: You can use def in for-each loops, though it’s optional. In each() closures, you don’t even need def – the closure parameter is implicitly dynamic.
Example 6: Def vs Explicit Type – What Changes
What we’re doing: Comparing behavior of def vs explicit type when reassigning.
Example 6: Def vs Typed
// With def - flexible
def flexible = "hello"
flexible = 42 // OK - def allows type change
println "Flexible: ${flexible}"
// With explicit type - strict
String strict = "hello"
try {
strict = 42 // Groovy will try to cast
println "Strict: ${strict}"
} catch (Exception e) {
println "Error: ${e.message}"
}
Output
Flexible: 42 Strict: 42
What happened here: Surprise – both work! When you assign an integer to a typed String variable, Groovy automatically converts it using toString(). However, with @CompileStatic enabled, the explicit type version would fail at compile time. That’s the difference between dynamic and static modes.
Example 7: Multiple Variable Declaration
What we’re doing: Declaring multiple variables using def.
Example 7: Multiple Variables
// Multiple assignments using destructuring
def (name, age, city) = ["Emma", 32, "Paris"]
println "Name: ${name}"
println "Age: ${age}"
println "City: ${city}"
Output
Name: Emma Age: 32 City: Paris
What happened here: Groovy supports multiple assignment (destructuring) with def. The list on the right gets unpacked into the variables on the left. This is a concise way to extract values from a list or a method that returns multiple values.
Example 8: Def with Null and Null Safety
What we’re doing: Handling null values with def.
Example 8: Null Handling
def value = null
println "Value: ${value}"
println "Is null: ${value == null}"
println "Type: ${value?.getClass()?.simpleName ?: 'null'}"
// Safe navigation with def variables
def user = null
println "User name: ${user?.name ?: 'Unknown'}"
// Assign a value
user = [name: "Charlie", email: "charlie@example.com"]
println "User name: ${user?.name ?: 'Unknown'}"
Output
Value: null Is null: true Type: null User name: Unknown User name: Charlie
What happened here: def variables can hold null. Groovy’s safe navigation operator (?.) and Elvis operator (?:) work perfectly with dynamically typed variables. No NullPointerException here.
Example 9: Def in Class Properties
What we’re doing: Using def in class property declarations.
Example 9: Class Properties
class Product {
def name
def price
def category
String toString() {
"${name} - \$${price} (${category})"
}
}
def laptop = new Product(name: "MacBook", price: 1299.99, category: "Electronics")
println laptop
def book = new Product(name: "Groovy in Action", price: 49.99, category: "Books")
println book
Output
MacBook - $1299.99 (Electronics) Groovy in Action - $49.99 (Books)
What happened here: Class properties can use def too. Groovy auto-generates getters and setters, and the map constructor works for setting properties. For production domain classes though, explicit types are recommended – especially with GORM, which needs types for database column mapping.
Pro Tip: In Groovy classes, properties declared without an access modifier (
def nameorString name) automatically get public getter and setter methods. You don’t need to write them.
Example 10: Def with Closures
What we’re doing: Storing closures in def variables.
Example 10: Def with Closures
def add = { a, b -> a + b }
def multiply = { a, b -> a * b }
def greet = { name -> "Hello, ${name}!" }
println "Add: ${add(3, 5)}"
println "Multiply: ${multiply(4, 6)}"
println "Greet: ${greet('World')}"
// Closures as first-class objects
def operations = [add: add, multiply: multiply]
println "Operations: add(10,20) = ${operations.add(10, 20)}"
Output
Add: 8 Multiply: 24 Greet: Hello, World! Operations: add(10,20) = 30
What happened here: Closures stored in def variables can be called just like methods. You can even store them in maps and invoke them dynamically. This is a taste of functional programming in Groovy.
Example 11: Def with the Implicit “it” Parameter
What we’re doing: Using closures where the single parameter is implicitly named it.
Example 11: Implicit it
def numbers = [1, 2, 3, 4, 5]
// 'it' is the implicit parameter name
def doubled = numbers.collect { it * 2 }
def evens = numbers.findAll { it % 2 == 0 }
def sum = numbers.inject(0) { total, num -> total + num }
println "Doubled: ${doubled}"
println "Evens: ${evens}"
println "Sum: ${sum}"
Output
Doubled: [2, 4, 6, 8, 10] Evens: [2, 4] Sum: 15
What happened here: When a closure takes a single parameter, Groovy provides the implicit name it. You don’t need to declare it – just use it directly. This works because the closure parameter is dynamically typed (like def).
Example 12: Def vs var (Groovy 5.0.6)
What we’re doing: Comparing def with Java’s var keyword in Groovy 5.0.6.
Example 12: def vs var
// def - dynamically typed (Object at compile time)
def defVar = "Hello"
println "def type: ${defVar.getClass().simpleName}"
// var - type inferred at compile time (Groovy 3.0+)
var varName = "Hello"
println "var type: ${varName.getClass().simpleName}"
// Both produce the same output, but var provides better IDE support
// because the type is inferred at compile time
def list1 = [1, 2, 3] // Compiled as Object
var list2 = [1, 2, 3] // Compiled as ArrayList
println "def list: ${list1.getClass().simpleName}"
println "var list: ${list2.getClass().simpleName}"
Output
def type: String var type: String def list: ArrayList var list: ArrayList
What happened here: Since Groovy 3.0, you can use var as a type-inferred local variable declaration – similar to Java 10’s var. The runtime behavior looks identical, but var gives the compiler and IDE more type information, resulting in better code completion and earlier error detection.
Def with Closures and Methods
Method Parameters: Def vs Typed
Method Parameters
// Untyped parameters - maximum flexibility
def process(data) {
println "Processing: ${data} (${data.getClass().simpleName})"
}
process("text")
process(42)
process([1, 2, 3])
// Typed parameter - documents expected input
String formatName(String first, String last) {
"${last}, ${first}"
}
println formatName("John", "Doe")
Output
Processing: text (String) Processing: 42 (Integer) Processing: [1, 2, 3] (ArrayList) Doe, John
Untyped parameters accept anything – great for utility methods. Typed parameters self-document the API and catch misuse earlier.
Static Typing with @CompileStatic
If you want the performance and safety of static typing without giving up Groovy syntax, swap the Groovy def keyword for explicit types and add @CompileStatic:
@CompileStatic
import groovy.transform.CompileStatic
@CompileStatic
class Calculator {
int add(int a, int b) {
return a + b
}
double divide(double a, double b) {
if (b == 0) throw new ArithmeticException("Division by zero")
return a / b
}
}
def calc = new Calculator()
println "Add: ${calc.add(10, 20)}"
println "Divide: ${calc.divide(10.0, 3.0)}"
Output
Add: 30 Divide: 3.3333333333333335
With @CompileStatic, Groovy checks types at compile time and generates bytecode similar to Java – faster execution but no dynamic features. Use it for performance-critical code. Learn more about Groovy’s type system in our Traits and File Operations guides.
Edge Cases and Best Practices
Edge Case: Def with Boolean Evaluation
Groovy Truth with Def
// Groovy truth - these are all "falsy"
def values = [null, 0, "", [], false, [:]]
values.each { val ->
println "${val?.toString()?.padRight(6) ?: 'null '} → ${val ? 'truthy' : 'falsy'}"
}
Output
null → falsy
0 → falsy
→ falsy
[] → falsy
false → falsy
[:] → falsy
Groovy has its own “truthiness” rules. Null, zero, empty strings, empty collections, and false are all falsy. This works well with def variables because the check happens at runtime.
Best Practices Summary
DO:
- Use
deffor local variables where the type is obvious from the value - Use explicit types for method parameters and return types in public APIs
- Use
@CompileStaticfor performance-critical code - Use
varwhen you want type inference with better IDE support
DON’T:
- Use
deffor GORM domain class properties (GORM needs explicit types) - Reassign variables to completely different types in production code
- Skip
defin scripts unless you intentionally want binding variables
Performance Considerations
Using def has a small runtime cost compared to explicit types. Every method call on a def variable goes through Groovy’s Meta-Object Protocol (MOP) for dynamic dispatch, while explicitly typed variables with @CompileStatic use direct JVM invocation.
For most applications, this difference is negligible. But for tight loops processing millions of records, consider using explicit types with @CompileStatic:
Performance Comparison
import groovy.transform.CompileStatic
// Dynamic (def) - slower for tight loops
long dynamicSum() {
def sum = 0L // 0L on purpose, see note below
for (def i = 0; i < 1_000_000; i++) {
sum += i
}
return sum
}
// Static (@CompileStatic) - Java-speed
@CompileStatic
long staticSum() {
long sum = 0
for (int i = 0; i < 1_000_000; i++) {
sum += i
}
return sum
}
def start1 = System.nanoTime()
println "Dynamic: ${dynamicSum()}"
def time1 = (System.nanoTime() - start1) / 1_000_000
def start2 = System.nanoTime()
println "Static: ${staticSum()}"
def time2 = (System.nanoTime() - start2) / 1_000_000
println "Dynamic time: ${time1}ms"
println "Static time: ${time2}ms"
Output
Dynamic: 499999500000 Static: 499999500000 Dynamic time: 172.9465ms Static time: 5.7152ms
The static version is significantly faster for numerical computation. Your exact timings will differ from run to run and machine to machine, but the gap stays wide. Also notice the 0L in the dynamic version. With a plain def sum = 0, Groovy starts the accumulator as an Integer, and the addition quietly wraps around past Integer.MAX_VALUE, printing 1783293664 instead of the correct sum. Dynamic typing does not protect you from integer overflow. But for most code – reading files, handling web requests, processing data – the difference is irrelevant. Use def by default and optimize with @CompileStatic only where profiling shows a bottleneck.
Common Pitfalls
Pitfall 1: Def in Class Fields vs Script Variables
Problem: Confusing scope when using def in different contexts.
Wrong Way
// In a script, this creates a local variable
def message = "hello"
void printMessage() {
println message // MissingPropertyException!
}
Solution:
Correct Way
// Without def, it goes into the script binding
message = "hello"
void printMessage() {
println message // Works - accesses binding
}
printMessage()
Output
hello
Pitfall 2: Using Def with Return Statements
Problem: Forgetting that Groovy methods return the last expression.
Subtle Return Behavior
def getStatus() {
def status = "active"
println "Setting status..."
// Oops - println returns null, and that becomes the return value
}
def result = getStatus()
println "Result: ${result}" // null, not "active"!
Output
Setting status... Result: null
Fix: Either use an explicit return status or make sure the last expression is what you want to return.
Conclusion
The Groovy def keyword is one of the language’s signature features. It removes type ceremony while keeping your code flexible and readable. It works for declaring variables, writing methods, and storing closures – letting you focus on what your code does rather than what types it uses.
The bottom line is balance. Use def for convenience in local code, explicit types for API clarity, and @CompileStatic when performance matters. Groovy gives you the choice – that’s what “optionally typed” means.
Try these examples in your IDE or Groovy Console. Experiment with changing def to explicit types and see how the behavior shifts.
Summary
defdeclares a dynamically typed variable – it compiles toObject- Groovy is optionally typed – you can use
defor explicit types interchangeably - In scripts,
defcreates local variables; withoutdef, variables go into the binding - Use
@CompileStaticfor Java-speed static typing within Groovy classes - Since Groovy 3.0,
varprovides type inference with better IDE support thandef
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy -e Command Line Option – Run Scripts Instantly
Frequently Asked Questions
What does def mean in Groovy?
The Groovy def keyword declares a variable, method, or property without specifying its type. It’s shorthand for Object – the variable’s actual type is determined at runtime based on the assigned value. It enables Groovy’s dynamic typing feature.
Is def the same as Object in Groovy?
Yes, under the hood, def compiles to java.lang.Object in the bytecode. The difference is syntactic – def is shorter and signals to other developers that you’re intentionally using dynamic typing. Both def and Object allow the variable to hold any type.
Should I use def or explicit types in Groovy?
Use def for local variables where the type is obvious from context. Use explicit types for public API method parameters and return types, GORM domain properties, and performance-critical code with @CompileStatic. Groovy is optionally typed, so you can mix both freely.
What is the difference between def and var in Groovy?
def makes a variable dynamically typed (compiled as Object). var (available since Groovy 3.0) uses type inference – the compiler determines the type from the assigned value. var gives better IDE code completion and catches type errors at compile time, while def defers everything to runtime.
Does using def make Groovy slower than Java?
For most applications, no noticeable difference. But in tight computational loops, def variables use dynamic dispatch which is slower than direct JVM method invocation. For performance-critical sections, use explicit types with @CompileStatic to get Java-equivalent speed.
Interview Questions on the Groovy Def Keyword
“Try each one aloud first; the phrasing you produce under mild pressure is what interviews measure.”
Q: A teammate’s Groovy script sets count = 0 at the top and updates it inside a helper method in the same file. It works. They “clean it up” to def count = 0 and the method starts throwing MissingPropertyException. What changed?
Scope. In a script, everything you write lives inside a generated run() method. With def, count is a local variable of run(), and methods defined in the script compile to separate methods on the script class, so they cannot see it. Without def, the assignment goes into the script’s Binding, which is shared state every script method can reach. That is why the “cleanup” broke it. If you want both declaration and visibility, use the @Field annotation to promote it to a real field.
Q: In code review you see a service class where every field, parameter, and return type is def. The class is called from Java. What do you flag?
At the bytecode level all of those become Object, so the Java callers lose every bit of type information and end up casting, and the compiler can no longer catch a wrong argument. Inside method bodies def is fine, but the public surface of a class is a contract, so I would ask for explicit parameter and return types there. It also documents intent for the next reader, enables IDE completion on the Java side, and opens the door to @CompileStatic later if the class becomes hot.
Q: A nightly Groovy job crashes with MissingMethodException on a line that has run fine for months. How can that happen, and what is your fix?
Because with dynamic typing, method resolution happens per call at runtime. The line was fine for every value it had ever seen; then some upstream change made a def variable carry a different type, say a String where a Date used to arrive, and the dispatch failed in production instead of at compile time. Short term, look at what feeds that variable. Long term, put @TypeChecked or @CompileStatic on classes that sit on data boundaries so this category of bug moves to build time, and add a test with the offending payload.
Q: You call an overloaded Java method, passing a def variable, and Groovy picks a different overload than the equivalent Java code. Why?
Java selects the overload at compile time from the declared type; Groovy selects it at runtime from the actual type, which is often called multimethod dispatch. So if an Object-typed reference holds a String, Java binds to the Object overload while Groovy invokes the String one. That is usually what you want, but it surprises Java developers and can change behavior when porting code. The follow-up worth volunteering: under @CompileStatic, Groovy switches to Java-style compile-time selection, so the same line can pick a different overload depending on the annotation.
Q: A profiler shows a tight numeric loop in your Groovy batch job is several times slower than the Java version. What do you do, and what is the trade-off?
Give the loop explicit types and annotate the class or method with @CompileStatic. That removes the dynamic call-site machinery and generates bytecode close to what javac would emit, which typically closes most of the gap. The trade-off is you give up dynamic features in that code: no runtime metaclass tricks, no methods added on the fly, and dispatch is now resolved at compile time. In practice you apply it surgically to hot paths and leave the rest of the codebase dynamic, which is exactly the flexibility Groovy’s optional typing is designed for.
Related Posts
Previous in Series: Setting Up Groovy IDE – IntelliJ IDEA, VS Code, and Eclipse
Next in Series: Groovy -e Command Line Option – Run Scripts Instantly
Related Topics You Might Like:
- Groovy Closures – The Complete Guide
- Groovy Traits – Reusable Behavior Composition
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment