Learn Groovy categories and mixins with 10+ tested examples. Add scoped methods to classes, compose behaviors, and extend JDK classes safely.
“Categories let you try on new methods like clothes – wear them when you need them, take them off when you’re done.”
Dave Thomas, The Pragmatic Programmer
Last Updated: March 2026 | Tested on: Groovy 5.x, 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
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 ['a','b','a','c','a','b'].histogram(): [a:3, b:2, c:1] Both categories active: 7 prime? true Today: March 08, 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.
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-03-08 14:30 Tomorrow: 2026-03-09 14:30 Next week: 2026-03-15 14:30 2 hours ago: 2026-03-08 12:30 6 months out: 2026-09-08 Meeting duration: 1 hour, 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.
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. 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 ***********om.com ***********word Outside: MissingMethodException === ExpandoMetaClass Approach === **************9012 ***********om.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
// 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: 12.34 ms Category method: 45.67 ms Ratio: 3.7x 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 2-4x slower than direct method calls because Groovy checks the thread-local category stack on each call. This is the trade-off for thread safety and automatic cleanup. For most real-world code (web requests, data processing, scripting), this overhead 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.
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