You patch String with ExpandoMetaClass for one report, and a week later that method is showing up in every corner of the app. Scoped extensions solve this. Groovy categories add methods that exist only inside a use block and vanish when it ends, while mixins compose behavior from several classes. This guide covers both, with tested Groovy 5 examples.
“Categories let you try on new methods like clothes – wear them when you need them, take them off when you’re done.”
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Intermediate | Reading Time: 18 minutes
Adding methods globally with ExpandoMetaClass is powerful but permanent (until cleanup). What if you want those extra methods to exist only within a specific block of code? Groovy categories give you scoped, temporary method extensions that automatically vanish when you leave the block – a safer alternative covered in our ExpandoMetaClass guide.
That’s where Groovy categories and Groovy mixins come in. Categories give you scoped, temporary method extensions that automatically vanish when you leave the block. Mixins let you permanently compose behaviors from multiple classes without the complexity of inheritance. Together, they offer safer, more controlled alternatives to ExpandoMetaClass.
In this tutorial, you’ll master both mechanisms with 10+ tested examples. We’ll build practical categories for string manipulation, date handling, and validation, then explore how mixins compose behavior from multiple sources. For the broader metaprogramming picture, see our Groovy Metaprogramming guide. The official Groovy metaprogramming documentation covers categories and mixins alongside other runtime techniques.
Table of Contents
What Are Categories and Mixins in Groovy?
Categories are classes that define static methods where the first parameter determines which class gets the method. When you use a category with Groovy’s use keyword, those static methods become available as instance methods on the target class – but only within the use block. Once the block ends, the methods disappear.
Mixins (via @Mixin annotation or runtime mixin()) permanently add methods from one class to another – similar to ExpandoMetaClass but with a more structured API. Note that @Mixin is deprecated since Groovy 2.3 in favor of Traits, but understanding mixins helps you work with legacy code.
According to the official Groovy documentation, categories are inspired by Objective-C categories and provide a clean way to add methods to existing classes without permanently modifying them.
Key Differences:
- Categories are temporary and scoped – methods exist only inside a
use { }block - Categories are thread-safe – each thread has its own category scope
- Mixins are permanent – once mixed in, the methods stay until JVM shutdown
- Mixins are deprecated – use Traits instead for new code
- Categories use static methods with the target type as the first parameter
- Categories can also be created with the
@Categoryannotation for cleaner syntax
The diagram below shows how Groovy resolves a method call inside nested use blocks, checking the innermost category first, then any enclosing categories, then the real class method, and finally throwing MissingMethodException if nothing matches:
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Quick Reference Table
| Feature | Categories | Mixins (@Mixin) | Traits |
|---|---|---|---|
| Scope | Block-scoped (use { }) | Permanent | Compile-time |
| Thread Safety | Thread-local | Not inherently | Yes |
| Syntax | Static methods / @Category | @Mixin / .mixin() | implements |
| Reversible? | Auto-reverts | No | No |
| IDE Support | Limited | Limited | Full |
| Status | Active, recommended | Deprecated (2.3+) | Modern, preferred |
| Applies To | Any class (incl. JDK) | Any class | Classes that implement |
| Multiple | Can stack/nest | Multiple classes | Multiple traits |
10 Practical Examples
Example 1: Your First Category – The use Block
What we’re doing: Creating a simple category that adds methods to String and demonstrating scope with the use keyword.
Example 1: Basic Category
// A category is a class with static methods
// The first parameter determines which class gets the method
class StringEnhancements {
static String shout(String self) {
self.toUpperCase() + '!!!'
}
static String whisper(String self) {
self.toLowerCase() + '...'
}
static int wordCount(String self) {
self.trim().split(/\s+/).length
}
static String reverse2(String self) {
self.reverse()
}
}
// Use the category inside a 'use' block
use(StringEnhancements) {
println 'hello world'.shout()
println 'PLEASE BE QUIET'.whisper()
println 'The quick brown fox'.wordCount()
println 'groovy'.reverse2()
}
// Outside the block, methods are gone
try {
'hello'.shout()
} catch (MissingMethodException e) {
println "Outside use block: shout() not available"
}
println "String class unchanged - no cleanup needed!"
Output
HELLO WORLD!!! please be quiet... 4 yvoorg Outside use block: shout() not available String class unchanged - no cleanup needed!
What happened here: We defined a category class with static methods. The first parameter of each method (String self) tells Groovy which class to attach the method to. Inside the use(StringEnhancements) block, those methods become available as instance methods on all strings. The moment the block ends, the methods vanish. No cleanup required, no global side effects. This is the safest form of metaprogramming in Groovy.
Example 2: The @Category Annotation – Cleaner Syntax
What we’re doing: Using the @Category annotation to write categories with instance-style syntax instead of static methods.
Example 2: @Category Annotation
@Category(String)
class TextFormatting {
// No 'self' parameter needed - 'this' refers to the String
String titleCase() {
this.split(/\s+/).collect { word ->
word[0].toUpperCase() + word[1..-1].toLowerCase()
}.join(' ')
}
String snakeCase() {
this.replaceAll(/([A-Z])/, '_$1').toLowerCase().replaceAll(/^_/, '')
}
String camelCase() {
def words = this.split(/[\s_-]+/)
words[0].toLowerCase() + words[1..-1].collect { it.capitalize() }.join()
}
String truncateWords(int maxWords) {
def words = this.split(/\s+/)
if (words.size() <= maxWords) return this
words.take(maxWords).join(' ') + '...'
}
}
use(TextFormatting) {
println 'hello world from groovy'.titleCase()
println 'myVariableName'.snakeCase()
println 'hello_beautiful_world'.camelCase()
println 'The quick brown fox jumps over the lazy dog'.truncateWords(5)
println 'Short text'.truncateWords(5)
}
Output
Hello World From Groovy my_variable_name helloBeautifulWorld The quick brown fox jumps... Short text
What happened here: The @Category(String) annotation tells Groovy this class defines category methods for String. Instead of writing static methods with String self as the first parameter, you write instance-style methods using this to refer to the target string. The annotation is syntactic sugar – behind the scenes, Groovy transforms these into static methods with the same signature as Example 1. The result is cleaner, more readable code.
Example 3: Categories for Multiple Classes
What we’re doing: Creating a single category that adds methods to multiple classes, and using multiple categories at once.
Example 3: Multi-Class Categories
// A category can target multiple classes
class MathHelpers {
// Methods for Integer
static boolean isPrime(Integer self) {
if (self < 2) return false
(2..Math.sqrt(self)).every { self % it != 0 }
}
static List factors(Integer self) {
(1..self).findAll { self % it == 0 }
}
// Methods for Number (works with BigDecimal, Double, etc.)
static double roundTo(Number self, int decimals) {
Math.round(self.toDouble() * 10**decimals) / 10**decimals
}
static String toPercent(Number self) {
"${(self.toDouble() * 100).round(1)}%"
}
// Methods for List
static double average(List self) {
self.sum() / self.size()
}
static Map histogram(List self) {
self.countBy { it }
}
}
use(MathHelpers) {
println "17 prime? ${17.isPrime()}"
println "12 prime? ${12.isPrime()}"
println "12 factors: ${12.factors()}"
println "3.14159.roundTo(2): ${3.14159.roundTo(2)}"
println "0.856.toPercent(): ${0.856.toPercent()}"
println "[1,2,3,4,5].average(): ${[1,2,3,4,5].average()}"
println "['a','b','a','c','a','b'].histogram(): ${['a','b','a','c','a','b'].histogram()}"
}
// Using multiple categories at once
class DateHelpers {
static String toReadable(Date self) {
self.format('MMMM dd, yyyy')
}
}
use(MathHelpers, DateHelpers) {
println "\nBoth categories active:"
println "7 prime? ${7.isPrime()}"
println "Today: ${new Date().toReadable()}"
}
Output
17 prime? true 12 prime? false 12 factors: [1, 2, 3, 4, 6, 12] 3.14159.roundTo(2): 3.14 0.856.toPercent(): 85.6% [1,2,3,4,5].average(): 3.0 ['a','b','a','c','a','b'].histogram(): [a:3, b:2, c:1] Both categories active: 7 prime? true Today: July 02, 2026
What happened here: A single category class can have methods targeting different classes – the first parameter type determines the target. We added methods to Integer, Double, and List in one category. You can also use multiple categories simultaneously by passing them all to use(). Each category’s methods are active within the block and automatically cleaned up when the block ends. Two small things about the output: average() prints 3.0 rather than 3 because the method is declared to return a double, and the Today line simply shows whatever date you run the script on.
Example 4: Nesting and Stacking Categories
What we’re doing: Demonstrating how use blocks can be nested, and how inner categories override outer ones.
Example 4: Nesting Categories
class FormalGreeting {
static String greet(String self) {
"Good day, ${self}. How do you do?"
}
}
class CasualGreeting {
static String greet(String self) {
"Hey ${self}! What's up?"
}
}
class ExtraFeatures {
static String excited(String self) {
self.toUpperCase() + '!!!'
}
}
// Outer category
use(FormalGreeting) {
println 'Alice'.greet()
// Inner category overrides the same method
use(CasualGreeting) {
println 'Bob'.greet()
// Can nest deeper
use(ExtraFeatures) {
println 'Charlie'.greet() // Still uses CasualGreeting
println 'wow'.excited() // ExtraFeatures is also available
}
// ExtraFeatures gone, but CasualGreeting still active
println 'Diana'.greet()
try {
'test'.excited()
} catch (MissingMethodException e) {
println "excited() gone after inner block"
}
}
// Back to FormalGreeting
println 'Eve'.greet()
}
Output
Good day, Alice. How do you do? Hey Bob! What's up? Hey Charlie! What's up? WOW!!! Hey Diana! What's up? excited() gone after inner block Good day, Eve. How do you do?
What happened here: Groovy categories can be nested. When an inner use block defines a method with the same name as an outer one, the inner category takes priority. Once the inner block ends, the outer category’s version is restored. You can also nest unrelated categories to combine functionality – ExtraFeatures added excited() alongside CasualGreeting‘s greet(). This stacking behavior makes categories composable and predictable.
Example 5: Built-In Groovy Categories
What we’re doing: Exploring Groovy’s built-in categories like TimeCategory, DOMCategory, and ServletCategory.
Example 5: Built-In Categories
import groovy.time.TimeCategory
// TimeCategory - the most commonly used built-in category
use(TimeCategory) {
def now = new Date()
// Natural date arithmetic
def tomorrow = now + 1.day
def nextWeek = now + 1.week
def twoHoursAgo = now - 2.hours
def halfYear = now + 6.months
println "Now: ${now.format('yyyy-MM-dd HH:mm')}"
println "Tomorrow: ${tomorrow.format('yyyy-MM-dd HH:mm')}"
println "Next week: ${nextWeek.format('yyyy-MM-dd HH:mm')}"
println "2 hours ago: ${twoHoursAgo.format('yyyy-MM-dd HH:mm')}"
println "6 months out: ${halfYear.format('yyyy-MM-dd')}"
// Duration arithmetic
def meeting = 1.hour + 30.minutes
println "\nMeeting duration: ${meeting}"
// Combine durations
def workDay = 8.hours
def lunch = 1.hour
def actualWork = workDay - lunch
println "Actual work time: ${actualWork}"
// More expressions
println "3.days in hours: ${3.days.toMilliseconds() / 3600000} hours"
println "90.minutes: ${90.minutes}"
}
// Outside the block - no more natural time syntax
try {
1.day
} catch (MissingPropertyException e) {
println "\nOutside use block: .day not available"
}
Output
Now: 2026-07-02 22:26 Tomorrow: 2026-07-03 22:26 Next week: 2026-07-09 22:26 2 hours ago: 2026-07-02 20:26 6 months out: 2027-01-02 Meeting duration: 1 hours, 30 minutes Actual work time: 7 hours 3.days in hours: 72 hours 90.minutes: 90 minutes Outside use block: .day not available
What happened here: TimeCategory is Groovy’s most popular built-in category. It adds properties like .day, .hours, .minutes, .weeks, and .months to Integer, and adds arithmetic operators to Date. This makes date calculations read like natural language – now + 1.day is much more readable than Java’s Calendar gymnastics. It’s a perfect example of how categories enhance readability within a controlled scope. The dates in the output track the day you run the script, and yes, Groovy really prints “1 hours, 30 minutes” – the toString() of groovy.time.TimeDuration does not singularize units.
Example 6: Practical Category – Validation Methods
What we’re doing: Building a practical validation category that adds validation methods to common types.
Example 6: Validation Category
class ValidationCategory {
// String validations
static boolean isEmail(String self) {
self ==~ /^[\w.+-]+@[\w-]+\.[\w.]+$/
}
static boolean isUrl(String self) {
self ==~ /^https?:\/\/[\w\-]+(\.[\w\-]+)+[\/\w\-._~:?#\[\]@!$&'()*+,;=]*$/
}
static boolean isAlphanumeric(String self) {
self ==~ /^[a-zA-Z0-9]+$/
}
static boolean isBlankOrNull(String self) {
self == null || self.trim().isEmpty()
}
static boolean hasMinLength(String self, int min) {
self != null && self.length() >= min
}
// Number validations
static boolean isPositive(Number self) {
self > 0
}
static boolean isBetween(Number self, Number low, Number high) {
self >= low && self <= high
}
// List validations
static boolean hasNoDuplicates(List self) {
self.size() == self.toSet().size()
}
}
use(ValidationCategory) {
// String validation
println "--- String Validation ---"
println "'user@test.com'.isEmail(): ${'user@test.com'.isEmail()}"
println "'not-an-email'.isEmail(): ${'not-an-email'.isEmail()}"
println "'https://groovy-lang.org'.isUrl(): ${'https://groovy-lang.org'.isUrl()}"
println "'abc123'.isAlphanumeric(): ${'abc123'.isAlphanumeric()}"
println "'abc 123'.isAlphanumeric(): ${'abc 123'.isAlphanumeric()}"
println "' '.isBlankOrNull(): ${' '.isBlankOrNull()}"
println "'hello'.hasMinLength(3): ${'hello'.hasMinLength(3)}"
// Number validation
println "\n--- Number Validation ---"
println "42.isPositive(): ${42.isPositive()}"
println "(-5).isPositive(): ${(-5).isPositive()}"
println "15.isBetween(10, 20): ${15.isBetween(10, 20)}"
println "25.isBetween(10, 20): ${25.isBetween(10, 20)}"
// List validation
println "\n--- List Validation ---"
println "[1,2,3].hasNoDuplicates(): ${[1,2,3].hasNoDuplicates()}"
println "[1,2,2,3].hasNoDuplicates(): ${[1,2,2,3].hasNoDuplicates()}"
// Combine validations
def validateUser = { name, email, age ->
def errors = []
if (name.isBlankOrNull()) errors << 'Name is required'
if (!name.hasMinLength(2)) errors << 'Name too short'
if (!email.isEmail()) errors << 'Invalid email'
if (!age.isBetween(18, 120)) errors << 'Invalid age'
errors ?: ['Valid']
}
println "\n--- Combined Validation ---"
println "Valid user: ${validateUser('Alice', 'alice@test.com', 30)}"
println "Invalid user: ${validateUser('', 'nope', 15)}"
}
Output
--- String Validation --- 'user@test.com'.isEmail(): true 'not-an-email'.isEmail(): false 'https://groovy-lang.org'.isUrl(): true 'abc123'.isAlphanumeric(): true 'abc 123'.isAlphanumeric(): false ' '.isBlankOrNull(): true 'hello'.hasMinLength(3): true --- Number Validation --- 42.isPositive(): true (-5).isPositive(): false 15.isBetween(10, 20): true 25.isBetween(10, 20): false --- List Validation --- [1,2,3].hasNoDuplicates(): true [1,2,2,3].hasNoDuplicates(): false --- Combined Validation --- Valid user: [Valid] Invalid user: [Name is required, Name too short, Invalid email, Invalid age]
What happened here: We built a full validation category that works across multiple types. The beauty of groovy categories is that validation reads naturally – 'user@test.com'.isEmail() is far more readable than EmailValidator.isValid('user@test.com'). And because it’s scoped to the use block, these methods don’t pollute the global namespace. This pattern is ideal for validation layers in applications.
Example 7: Categories with Return Values and the use Block
What we’re doing: Showing that use blocks return the value of the last expression, making them useful in functional contexts.
Example 7: use Block Return Values
class CollectionEnhancements {
static List chunked(List self, int size) {
self.collate(size)
}
static Map zipWithIndex(List self) {
self.withIndex().collectEntries { val, idx -> [(idx): val] }
}
static List interleave(List self, List other) {
def result = []
def maxLen = Math.max(self.size(), other.size())
(0..<maxLen).each { i ->
if (i < self.size()) result << self[i]
if (i < other.size()) result << other[i]
}
result
}
static def secondLargest(List self) {
self.unique().sort()[-2]
}
}
// use blocks return the last expression's value
def chunks = use(CollectionEnhancements) {
[1, 2, 3, 4, 5, 6, 7, 8, 9].chunked(3)
}
println "Chunks: ${chunks}"
def indexed = use(CollectionEnhancements) {
['apple', 'banana', 'cherry'].zipWithIndex()
}
println "Indexed: ${indexed}"
def mixed = use(CollectionEnhancements) {
[1, 3, 5].interleave([2, 4, 6])
}
println "Interleaved: ${mixed}"
def second = use(CollectionEnhancements) {
[10, 5, 20, 15, 3].secondLargest()
}
println "Second largest: ${second}"
// Useful in pipelines
def result = use(CollectionEnhancements) {
(1..20).toList()
.chunked(5)
.collect { chunk -> chunk.sum() }
}
println "Chunk sums: ${result}"
Output
Chunks: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] Indexed: [0:apple, 1:banana, 2:cherry] Interleaved: [1, 2, 3, 4, 5, 6] Second largest: 15 Chunk sums: [15, 40, 65, 90]
What happened here: The use block is an expression – it returns the value of its last statement. This makes categories work well in functional pipelines and assignments. You get the methods you need, compute a result, and the result escapes the block while the methods don’t. This pattern keeps your code clean: the category is active only for the exact scope where you need it.
Example 8: Runtime Mixins – Permanent Method Injection
What we’re doing: Using the runtime mixin() method to permanently add methods from one class to another.
Example 8: Runtime Mixins
// Mixin source classes - regular classes with instance methods
class Serializable2 {
String toJson() {
def props = this.properties.findAll { k, v -> k != 'class' && !k.contains('__') && k != 'auditInfo' }
.sort { it.key }
def pairs = props.collect { k, v -> "\"${k}\": \"${v}\"" }
"{ ${pairs.join(', ')} }"
}
}
class Auditable {
String getAuditInfo() {
"[${this.class.simpleName}] inspected at ${new Date().format('HH:mm:ss')}"
}
}
class Describable {
String describe() {
def props = this.properties.findAll { k, v -> k != 'class' && !k.contains('__') && k != 'auditInfo' }
.sort { it.key }
.collect { k, v -> "${k}=${v}" }
"${this.class.simpleName}(${props.join(', ')})"
}
}
// Target class
class Product {
String name
double price
String category
}
// Mix in multiple behaviors permanently
Product.mixin(Serializable2, Auditable, Describable)
def laptop = new Product(name: 'MacBook', price: 1999.99, category: 'Electronics')
println laptop.toJson()
println laptop.auditInfo
println laptop.describe()
// Another instance also gets the methods
def mouse = new Product(name: 'Mouse', price: 29.99, category: 'Accessories')
println mouse.describe()
println mouse.toJson()
// Note: @Mixin annotation is deprecated since Groovy 2.3
// Use Traits instead for new code
println "\nNote: Prefer Traits over @Mixin for new code"
Output
{ "category": "Electronics", "name": "MacBook", "price": "1999.99" }
[Product] inspected at 22:18:21
Product(category=Electronics, name=MacBook, price=1999.99)
Product(category=Accessories, name=Mouse, price=29.99)
{ "category": "Accessories", "name": "Mouse", "price": "29.99" }
Note: Prefer Traits over @Mixin for new code
What happened here: The runtime mixin() method takes methods from one or more classes and adds them to the target class permanently. Inside the mixed-in methods, this refers to the target object (the Product instance), not the source class. This allows the methods to access the target’s fields. The audit timestamp comes from new Date(), so expect a different time on every run. While groovy mixins work well, they’re officially deprecated – Traits provide the same composition with better compile-time safety.
Example 9: Category for Testing – Mock-Friendly Methods
What we’re doing: Using categories to temporarily override behavior in tests, making them thread-safe and self-cleaning.
Example 9: Testing with Categories
// Production code
class WeatherService {
String getWeather(String city) {
// In real code, this would call an external API
"Fetching weather for ${city} from API..."
}
}
class NotificationService {
boolean sendEmail(String to, String subject) {
// In real code, this would send an actual email
println "SENDING REAL EMAIL to ${to}"
true
}
}
// Test categories - override methods for testing
class TestWeatherOverrides {
static String getWeather(WeatherService self, String city) {
// Return predictable test data
switch (city) {
case 'London': return 'Rainy, 12C'
case 'Tokyo': return 'Sunny, 25C'
default: return 'Unknown city'
}
}
}
class TestNotificationOverrides {
static sent = []
static boolean sendEmail(NotificationService self, String to, String subject) {
// Don't send real emails! Just record the call
sent << [to: to, subject: subject]
true
}
}
// "Test" using categories - no real API calls or emails
use(TestWeatherOverrides, TestNotificationOverrides) {
def weather = new WeatherService()
def notifier = new NotificationService()
// These use test implementations, not real ones
def londonWeather = weather.getWeather('London')
def tokyoWeather = weather.getWeather('Tokyo')
println "London: ${londonWeather}"
println "Tokyo: ${tokyoWeather}"
notifier.sendEmail('alice@test.com', 'Weather Report')
notifier.sendEmail('bob@test.com', 'Daily Update')
// Verify calls were recorded
println "\nEmails sent during test:"
TestNotificationOverrides.sent.each { println " To: ${it.to}, Subject: ${it.subject}" }
println "Total emails: ${TestNotificationOverrides.sent.size()}"
}
// Outside the block, real implementations are restored
println "\nOutside test: ${new WeatherService().getWeather('Paris')}"
Output
London: Rainy, 12C Tokyo: Sunny, 25C Emails sent during test: To: alice@test.com, Subject: Weather Report To: bob@test.com, Subject: Daily Update Total emails: 2 Outside test: Fetching weather for Paris from API...
What happened here: Categories are perfect for testing because they’re scoped, thread-safe, and self-cleaning. We overrode the weather API and email service with test doubles that return predictable data and record calls. No real HTTP requests or emails were sent. After the use block, everything returns to normal. This pattern is especially useful for integration tests where you need to stub external services without a full mocking framework.
Example 10: Building a DSL with Categories
What we’re doing: Using categories to create a domain-specific language for defining configurations in a natural, readable way.
Example 10: DSL with Categories
class DslEnhancements {
// Add units to numbers
static Map megabytes(Integer self) { [size: self * 1024 * 1024, unit: 'bytes'] }
static Map gigabytes(Integer self) { [size: self * 1024 * 1024 * 1024L, unit: 'bytes'] }
static Map seconds(Integer self) { [duration: self * 1000, unit: 'ms'] }
static Map minutes(Integer self) { [duration: self * 60 * 1000, unit: 'ms'] }
static Map percent(Integer self) { [value: self / 100.0, unit: 'ratio'] }
// Convert strings to typed config values
static Map asPort(Integer self) {
if (self < 1 || self > 65535) throw new IllegalArgumentException("Invalid port: ${self}")
[port: self, valid: true]
}
static Map asHost(String self) { [host: self, resolved: true] }
}
class ServerConfig {
Map settings = [:]
void set(String key, value) { settings[key] = value }
String toString() {
settings.collect { k, v -> " ${k}: ${v}" }.join('\n')
}
}
use(DslEnhancements) {
def config = new ServerConfig()
// DSL-style configuration
config.set('maxMemory', 512.megabytes())
config.set('diskQuota', 2.gigabytes())
config.set('timeout', 30.seconds())
config.set('sessionTTL', 15.minutes())
config.set('cpuThreshold', 80.percent())
config.set('port', 8080.asPort())
config.set('host', 'api.example.com'.asHost())
println "Server Configuration:"
println config
// The DSL reads naturally:
println "\nReadable expressions:"
println " 512 MB = ${512.megabytes().size} bytes"
println " 30 sec = ${30.seconds().duration} ms"
println " 80% = ${80.percent().value} ratio"
}
Output
Server Configuration: maxMemory: [size:536870912, unit:bytes] diskQuota: [size:2147483648, unit:bytes] timeout: [duration:30000, unit:ms] sessionTTL: [duration:900000, unit:ms] cpuThreshold: [value:0.8, unit:ratio] port: [port:8080, valid:true] host: [host:api.example.com, resolved:true] Readable expressions: 512 MB = 536870912 bytes 30 sec = 30000 ms 80% = 0.8 ratio
What happened here: We used categories to add unit methods to Integer and String, creating a DSL where 512.megabytes() and 30.seconds() read like natural language. This is a common pattern in Groovy DSLs – Gradle uses similar techniques for its build scripts. The category scope ensures these DSL methods exist only where they’re needed, keeping the rest of your code clean.
Example 11 (Bonus): Category vs ExpandoMetaClass – Side by Side
What we’re doing: Comparing the same functionality implemented with categories vs ExpandoMetaClass to highlight the practical differences.
Example 11: Category vs EMC Comparison
// === APPROACH 1: Category ===
class StringCategory {
static String mask(String self, int visible = 4) {
if (self.length() <= visible) return self
'*' * (self.length() - visible) + self[-visible..-1]
}
}
println "=== Category Approach ==="
use(StringCategory) {
println '4532-1234-5678-9012'.mask() // Show last 4
println 'alice@example.com'.mask(6) // Show last 6
println 'secret-password'.mask()
}
// Outside: clean, no side effects
try { 'test'.mask() } catch (e) { println "Outside: ${e.class.simpleName}" }
// === APPROACH 2: ExpandoMetaClass ===
println "\n=== ExpandoMetaClass Approach ==="
String.metaClass.mask = { int visible = 4 ->
if (delegate.length() <= visible) return delegate
'*' * (delegate.length() - visible) + delegate[-visible..-1]
}
println '4532-1234-5678-9012'.mask()
println 'alice@example.com'.mask(6)
println 'secret-password'.mask()
// Must manually clean up
GroovySystem.metaClassRegistry.removeMetaClass(String)
// === COMPARISON ===
println "\n=== Comparison ==="
println "Category: Scoped ✓ Thread-safe ✓ Auto-cleanup ✓ 'self' parameter"
println "EMC: Global ✓ Thread-safe ✗ Manual cleanup 'delegate' keyword"
println "\nUse Category when: scope matters, testing, thread safety needed"
println "Use EMC when: you need methods available everywhere, building frameworks"
Output
=== Category Approach === ***************9012 ***********le.com ***********word Outside: MissingMethodException === ExpandoMetaClass Approach === ***************9012 ***********le.com ***********word === Comparison === Category: Scoped ✓ Thread-safe ✓ Auto-cleanup ✓ 'self' parameter EMC: Global ✓ Thread-safe ✗ Manual cleanup 'delegate' keyword Use Category when: scope matters, testing, thread safety needed Use EMC when: you need methods available everywhere, building frameworks
What happened here: The same mask() method works identically in both approaches, but the lifecycle is completely different. Categories are self-cleaning and thread-safe; EMC is global and requires manual cleanup. Categories use self as a method parameter; EMC uses delegate inside closures. Choose based on your needs: categories for scoped, safe operations; EMC for permanent, global extensions.
Categories vs Mixins vs ExpandoMetaClass vs Traits
Groovy offers multiple ways to add methods to classes. Here’s a full comparison to help you choose the right approach:
Decision Guide
WHEN TO USE EACH APPROACH:
──────────────────────────
Categories (use { })
✓ Temporary method additions in a specific scope
✓ Thread-safe method extensions
✓ Testing (override methods during tests)
✓ DSLs with scoped vocabulary
✗ Not available outside the use block
Mixins (@Mixin / .mixin())
✓ Legacy code that needs permanent method injection
✗ DEPRECATED since Groovy 2.3 - use Traits instead
✗ Not thread-safe
✗ No compile-time checking
ExpandoMetaClass (.metaClass)
✓ Adding methods globally at runtime
✓ Framework-level extensions
✓ Adding methods to JDK classes
✗ Requires manual cleanup
✗ Not thread-safe
✗ Global side effects
Traits (implements)
✓ Compile-time behavior composition
✓ Full IDE support and type checking
✓ Works with @CompileStatic
✓ Clean inheritance model
✗ Must be declared at compile time
✗ Can't add to JDK classes after compilation
For new code, the recommendation is clear: use Traits for compile-time behavior composition, Categories for scoped runtime extensions, and ExpandoMetaClass only when you need truly dynamic, global changes. Avoid @Mixin in new code. For a deeper understanding of Traits, see our upcoming guide on Groovy Traits.
Advanced Patterns
Composing Categories as Modules
Category Modules Pattern
// Define categories as focused modules
class StringModule {
static String capitalize2(String self) { self[0].toUpperCase() + self[1..-1] }
static boolean isPalindrome(String self) {
def clean = self.toLowerCase().replaceAll('[^a-z0-9]', '')
clean == clean.reverse()
}
}
class NumberModule {
static String toOrdinal(Integer self) {
def suffixes = ['th', 'st', 'nd', 'rd']
def mod100 = self % 100
def suffix = (mod100 in 11..13) ? 'th' : suffixes[Math.min(self % 10, 3)]
"${self}${suffix}"
}
}
class CollectionModule {
static def second(List self) { self.size() >= 2 ? self[1] : null }
static def last2(List self) { self.takeRight(2) }
}
// Compose modules for your context
def withAllModules = { Closure action ->
use(StringModule, NumberModule, CollectionModule, action)
}
withAllModules {
println "capitalize: ${'hello'.capitalize2()}"
println "palindrome: ${'racecar'.isPalindrome()}"
println "ordinal: ${1.toOrdinal()}, ${2.toOrdinal()}, ${3.toOrdinal()}, ${11.toOrdinal()}, ${21.toOrdinal()}"
println "second: ${['a','b','c'].second()}"
println "last2: ${[1,2,3,4,5].last2()}"
}
Output
capitalize: Hello palindrome: true ordinal: 1st, 2nd, 3rd, 11th, 21st second: b last2: [4, 5]
Organizing categories as focused modules makes them reusable and composable. The withAllModules helper wraps the use call so consuming code doesn’t need to know which categories are involved. This is a clean way to build domain-specific method libraries that you can assemble differently in different contexts.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Prefer categories over ExpandoMetaClass when scope control matters
- Use the
@Categoryannotation for cleaner instance-style syntax - Organize categories as small, focused modules with related methods
- Use categories in testing to stub methods without global side effects
- use Groovy’s built-in
TimeCategoryfor date arithmetic
DON’T:
- Use
@Mixinin new code – use Traits instead - Nest
useblocks too deeply – it becomes hard to reason about which methods are active - Use categories as a replacement for proper class design – if you always need the methods, define them on the class
- Mix categories with
@CompileStatic– statically compiled code can’t see category methods
Performance Considerations
Categories have a performance cost because Groovy must check the category stack on every method call within the use block.
Category Performance
class PerfCategory {
static int double2(Integer self) { self * 2 }
}
def iterations = 100_000
// Warm up the JVM first so JIT compilation does not skew either loop
iterations.times { 42 * 2 }
use(PerfCategory) { iterations.times { 42.double2() } }
// Without category - direct method call
def start1 = System.nanoTime()
iterations.times { 42 * 2 }
def direct = (System.nanoTime() - start1) / 1_000_000.0
// With category
def start2 = System.nanoTime()
use(PerfCategory) {
iterations.times { 42.double2() }
}
def withCat = (System.nanoTime() - start2) / 1_000_000.0
println "Direct multiplication: ${String.format('%.2f', direct)} ms"
println "Category method: ${String.format('%.2f', withCat)} ms"
println "Ratio: ${String.format('%.1f', withCat / direct)}x"
println "\nFor most applications:"
println " - Category overhead is negligible for I/O-bound code"
println " - For CPU-intensive inner loops, keep categories outside the loop"
println " - The safety benefits usually outweigh the performance cost"
Output
Direct multiplication: 71.40 ms Category method: 255.87 ms Ratio: 3.6x For most applications: - Category overhead is negligible for I/O-bound code - For CPU-intensive inner loops, keep categories outside the loop - The safety benefits usually outweigh the performance cost
Categories are noticeably slower than direct method calls because Groovy checks the thread-local category stack on each call. In our runs on Groovy 5.0.6 (the version at the time of writing) the ratio ranged from about 2.5x to 6x, and exact timings swing between runs and machines, so treat these numbers as a rough guide. The warmup step matters: without it, the first loop absorbs all the JIT compilation cost and can even make the direct call look slower than the category.
This overhead is the trade-off for thread safety and automatic cleanup. For most real-world code (web requests, data processing, scripting), it is invisible. For tight computational loops, consider using the method directly or moving the computation outside the use block.
Common Pitfalls
Pitfall 1: Forgetting the First Parameter Type
First Parameter Pitfall
// WRONG: Missing the 'self' parameter
class BadCategory {
// This won't work as a category method!
static String shout() {
"SHOUTING" // No 'self' - which class does this apply to?
}
// Also wrong: wrong parameter type
static String greet(Object self) {
"Hello ${self}"
// Works, but applies to ALL objects, not just String
}
}
// CORRECT: Specify the target class as the first parameter
class GoodCategory {
static String shout(String self) {
self.toUpperCase() + '!!!'
}
static String greet(String self) {
"Hello ${self}"
}
}
use(GoodCategory) {
println 'hello'.shout()
println 'World'.greet()
}
// With @Category, you don't have this problem:
@Category(String)
class AlsoGoodCategory {
String whisper() {
this.toLowerCase() + '...'
}
}
use(AlsoGoodCategory) {
println 'QUIET PLEASE'.whisper()
}
Output
HELLO!!! Hello World quiet please...
The first parameter of a category method is special – its type determines which class the method is added to. Forgetting it or using Object are common mistakes. If you use Object, the method applies to every class, which is rarely what you want. The @Category annotation avoids this issue entirely by specifying the target class in the annotation.
Pitfall 2: Categories Don’t Work with @CompileStatic
CompileStatic Pitfall
class MyCategory {
static String enhanced(String self) { "enhanced: ${self}" }
}
// This works (dynamic compilation)
def dynamicResult = use(MyCategory) {
'hello'.enhanced()
}
println "Dynamic: ${dynamicResult}"
// This would NOT compile with @CompileStatic:
// @CompileStatic
// def staticMethod() {
// use(MyCategory) {
// 'hello'.enhanced() // COMPILE ERROR: Cannot find matching method
// }
// }
// Workaround: use @CompileStatic on the surrounding code,
// but leave the use block in a dynamic method
println "Keep category usage in dynamic (non-@CompileStatic) methods"
Output
Dynamic: enhanced: hello Keep category usage in dynamic (non-@CompileStatic) methods
Categories are a runtime mechanism and don’t work with @CompileStatic, which resolves methods at compile time. If you need both static compilation and method extension, consider Groovy Extension Modules (which register methods at the classpath level) or Traits (which are compile-time constructs). Keep your category-using code in dynamically compiled methods.
Conclusion
We’ve covered both Groovy categories and Groovy mixins in depth – from basic use blocks to advanced patterns like module composition, testing overrides, and DSL building. Categories are one of Groovy’s most elegant features: they give you the power of metaprogramming with the safety of scoped, thread-local, self-cleaning method extensions.
The key insight is this: use categories when you want temporary, safe method extensions. Use ExpandoMetaClass when you need permanent, global changes. And for new code that needs compile-time behavior composition, use Traits. Each tool has its place, and knowing which to reach for makes you a more effective Groovy developer.
For the broader metaprogramming picture, see our Groovy Metaprogramming guide. To learn about Traits – the modern replacement for mixins – check out our upcoming guide on Groovy Traits. The official Groovy metaprogramming documentation has additional details on both categories and the deprecated mixin mechanism.
Summary
- Categories add methods to classes within a scoped
use { }block – methods auto-revert when the block ends - Category methods are static methods where the first parameter type determines the target class
- The
@Categoryannotation provides cleaner instance-style syntax - Categories are thread-safe (thread-local) and require no cleanup – safer than ExpandoMetaClass
@Mixinis deprecated – use Traits for compile-time behavior composition instead
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 Traits – Reusable Behavior Composition
Frequently Asked Questions
What are categories in Groovy?
Categories are a Groovy metaprogramming mechanism that lets you add methods to existing classes within a scoped ‘use’ block. You define a category class with static methods where the first parameter type determines which class gets the method. Inside the use block, those methods are available as instance methods. When the block ends, the methods automatically disappear. Categories are thread-safe and require no cleanup.
What is the difference between categories and ExpandoMetaClass?
Categories (use blocks) are scoped and temporary – methods exist only inside the block and auto-revert. ExpandoMetaClass changes are global and permanent until manually removed. Categories are thread-safe (thread-local); EMC is not inherently thread-safe. Categories use static methods with a ‘self’ parameter; EMC uses closures with ‘delegate’. Choose categories for scoped, safe extensions; EMC for permanent, global changes.
Are Groovy mixins deprecated?
Yes. The @Mixin annotation has been deprecated since Groovy 2.3. The runtime mixin() method still works but is not recommended for new code. Groovy Traits are the modern replacement – they provide compile-time behavior composition with full IDE support, type checking, and compatibility with @CompileStatic. Use Traits for new code that needs behavior composition.
Can I use multiple categories at once in Groovy?
Yes. Pass multiple category classes to the use() keyword: use(CategoryA, CategoryB, CategoryC) { … }. All methods from all categories are active inside the block. You can also nest use blocks – inner categories override outer ones for methods with the same name. When the inner block ends, the outer category’s version is restored.
Do Groovy categories work with @CompileStatic?
No. Categories are a runtime mechanism that relies on Groovy’s Meta-Object Protocol (MOP), which @CompileStatic bypasses. If you need method extensions with static compilation, consider Groovy Extension Modules (which register methods at the classpath level) or Traits (which are compile-time constructs). Keep category-using code in dynamically compiled methods.
Interview Questions on Groovy Categories and Mixins
“Interviewers rarely ask for definitions. They ask what happens in situations like these.”
Q: A teammate wraps some date math in use(TimeCategory) inside a Jenkins shared library. It works in a sequential stage, but a parallel branch throws MissingPropertyException on 1.day. What is going on?
Categories are thread-local. The use block registers the category on the current thread’s call stack, so any code that runs on a different thread, like a parallel pipeline branch or an executor pool task, never sees those methods. The fix is to open the use block inside the code that actually runs on the worker thread, or drop TimeCategory and use java.time arithmetic, which needs no metaprogramming at all. The gotcha interviewers listen for: thread-local scoping is exactly what makes categories safe, and it is also what breaks them across threads.
Q: You write a category method static String pad(Integer self) and call 5L.pad() inside the use block. It fails. Why, and what does that tell you about category dispatch?
Category dispatch keys off the declared type of the first parameter. A method declared for Integer attaches to Integer only, and 5L is a Long, so there is no match and Groovy throws MissingMethodException. The practical answer is to declare the parameter as a wider type, Number in this case, so all numeric boxes qualify. This also explains a classic surprise: decimal literals in Groovy are BigDecimal by default, so a category written for Double silently misses 3.14. Knowing which runtime type a literal produces is half of debugging category problems.
Q: Your team migrates a Groovy service to @CompileStatic for performance, and every class that relies on categories stops compiling. What is your recommendation?
Categories are a runtime MOP feature, and @CompileStatic resolves every call at compile time, so category methods simply do not exist as far as the static compiler is concerned. I would not fight it. For methods that should feel built in, move them to a Groovy extension module, which registers methods on the classpath and is visible to the static compiler. For behavior composition, use traits. If a small piece of code genuinely needs categories, isolate it in a dynamically compiled method and keep @CompileStatic on the rest.
Q: During a code review you find legacy classes using @Mixin. The tech lead asks whether it is worth migrating. What do you say?
Yes, and the migration target is traits. @Mixin has been deprecated since Groovy 2.3; it injects behavior at runtime, so you get no IDE completion, no static type checking, and awkward debugging. Traits do the same composition at compile time, work with @CompileStatic, and have clear conflict rules: when two traits declare the same method, the last one in the implements clause wins, and you can override explicitly. The one thing to check during migration is stateful mixins, because trait fields get name-mangled into the implementing class, which can affect serialization and reflection code.
Q: A profiler shows a hot loop running noticeably slower when it executes inside a use block, even for calls that are not category methods. Can you explain that?
While a category is active, Groovy has to consult the thread-local category stack as part of method resolution, and that check applies to dynamic calls inside the block, not just the category methods themselves. In a tight loop, that per-call overhead adds up. My approach is to keep use blocks narrow: do the category-flavored work once, outside the loop if possible, or replace the category with an extension module or a plain static helper for hot paths. Categories buy readability and safety, and the honest trade is that they are the slowest of Groovy’s extension mechanisms.
Related Posts
Previous in Series: Groovy ExpandoMetaClass – Add Methods at Runtime
Next in Series: Groovy Traits – Reusable Behavior Composition
Related Topics You Might Like:
- Groovy Metaprogramming – Runtime Magic Explained
- Groovy ExpandoMetaClass – Add Methods at Runtime
- Groovy Traits – Reusable Behavior Composition
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment