Groovy Categories and Mixins – Extend Existing Classes with 10+ Examples

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.

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 @Category annotation for cleaner syntax

Quick Reference Table

FeatureCategoriesMixins (@Mixin)Traits
ScopeBlock-scoped (use { })PermanentCompile-time
Thread SafetyThread-localNot inherentlyYes
SyntaxStatic methods / @Category@Mixin / .mixin()implements
Reversible?Auto-revertsNoNo
IDE SupportLimitedLimitedFull
StatusActive, recommendedDeprecated (2.3+)Modern, preferred
Applies ToAny class (incl. JDK)Any classClasses that implement
MultipleCan stack/nestMultiple classesMultiple 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 @Category annotation 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 TimeCategory for date arithmetic

DON’T:

  • Use @Mixin in new code – use Traits instead
  • Nest use blocks 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 @Category annotation provides cleaner instance-style syntax
  • Categories are thread-safe (thread-local) and require no cleanup – safer than ExpandoMetaClass
  • @Mixin is 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.

Previous in Series: Groovy ExpandoMetaClass – Add Methods at Runtime

Next in Series: Groovy Traits – Reusable Behavior Composition

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *