Skip to content

Groovy Categories — Scoped Method Injection and DSL Building

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Groovy Categories. We cover key concepts, practical examples, and best practices to help you master this topic.

Groovy categories provide a scoped mechanism to add new methods to existing classes within use blocks, enabling temporary extensions without modifying metaclass globally.

What You'll Learn

  • Defining category classes
  • The use block scope
  • Category method conventions
  • Building DSLs with categories

Why It Matters

Categories let you extend closed classes temporarily without global side effects. DodaZIP uses categories to add compression-specific operations to standard Java I/O classes during archive processing.

Real-World Use

Domain-Specific Languages, testing helpers, temporary utility methods during processing pipelines, and framework-level extensions without permanent class pollution.

flowchart LR
    A["Category"] --> B["Category Class"]
    B --> C["Static Methods"]
    C --> D["First Param = Self"]
    A --> E["use Block"]
    E --> F["Scoped Extension"]
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af

Category Class Structure

A category class defines static methods where the first parameter is the target object:

class IntMath {
    static int square(Integer self) {
        self * self
    }

    static int cube(Integer self) {
        self * self * self
    }

    static boolean isEven(Integer self) {
        self % 2 == 0
    }
}

use(IntMath) {
    println 5.square()   // 25
    println 3.cube()     // 27
    println 4.isEven()   // true
}

Multiple Categories

Combine multiple categories in one use block:

class StringOps {
    static String reverseWords(String self) {
        self.split(' ').reverse().join(' ')
    }
}

class ListOps {
    static <T> T random(List<T> self) {
        self[new Random().nextInt(self.size())]
    }
}

use(StringOps, ListOps) {
    println "hello world".reverseWords()  // world hello
    println [1, 2, 3, 4, 5].random()     // random element
}

Category with Closures

Categories can receive closures for more flexible APIs:

class FileUtils {
    static void withEachLine(File self, Closure closure) {
        self.eachLine { line ->
            closure(line)
        }
        println "Processed: ${self.name}"
    }
}

use(FileUtils) {
    def file = new File('data.txt')
    file.withEachLine { line ->
        println ">> $line"
    }
}

Building a Simple DSL

Categories enable clean DSLs:

class SqlDSL {
    static StringBuilder select(StringBuilder self, String... columns) {
        self.append("SELECT ${columns.join(', ')} ")
    }

    static StringBuilder from(StringBuilder self, String table) {
        self.append("FROM $table ")
    }

    static StringBuilder where(StringBuilder self, String condition) {
        self.append("WHERE $condition ")
    }

    static StringBuilder orderBy(StringBuilder self, String column) {
        self.append("ORDER BY $column")
    }
}

use(SqlDSL) {
    def query = new StringBuilder()
    query.select('id', 'name').from('users').where('active = true').orderBy('name')
    println query.toString()
    // SELECT id, name FROM users WHERE active = true ORDER BY name
}

Category Method Resolution

Methods added by categories have lower priority than existing methods:

class IntExtensions {
    static int abs(Integer self) {
        println "Category abs called"
        Math.abs(self)
    }
}

use(IntExtensions) {
    // If Integer already has abs(), category version is ignored
    println (-5).abs()  // 5 (uses Integer.abs(), not category)
}

Nested use Blocks

class MathCategory {
    static int doubleIt(Integer self) { self * 2 }
}

class StringCategory {
    static String doubleIt(String self) { self + self }
}

use(MathCategory) {
    println 5.doubleIt()  // 10
    use(StringCategory) {
        println "hi".doubleIt()  // hihi
    }
}

Nested blocks isolate extensions to their respective scopes.

Common Mistakes

1. Forgetting the first parameter must be the target type

// Wrong — first param must match the extended type
class WrongCategory {
    static String shout(String self, String extra) { self + extra }
}

2. Category methods shadowing existing methods

Existing methods always win. Categories cannot override existing behavior.

3. Overusing categories for permanent extensions

If you need the method everywhere, use ExpandoMetaClass instead of wrapping code in use blocks.

4. Performance overhead of use blocks

Each use block creates a CategoryMethod meta-class. Avoid wrapping hot loops in use.

5. Forgetting category methods are static

Non-static methods in a category class are ignored by the use mechanism.

Practice Questions

1. What is the first parameter of a category method?

The target object type. When you call obj.method(), obj is passed as the first parameter.

2. Can categories override existing methods?

No. Existing methods always take precedence over category-provided methods.

3. How do you apply multiple categories at once?

Pass them as comma-separated arguments to use: use(Cat1, Cat2) { ... }.

4. What happens if you nest use blocks with the same method name?

Each block has its own scope. Inner categories are only active within the inner block.

Challenge: Create a category that adds toJson and fromJson methods to Map and List.

FAQ

{{< faq question="What is the difference between categories and ExpandoMetaClass?" >}} Categories are scoped to use blocks. ExpandoMetaClass is global. Categories cannot override existing methods; ExpandoMetaClass can. {{< /faq >}}

{{< faq question="Are categories thread-safe?" >}} Yes, because each thread has its own use block scope. Category methods are static with no shared state. {{< /faq >}}

{{< faq question="Can categories extend Java classes?" >}} Yes. Categories work with any class, including Java standard library classes. {{< /faq >}}

{{< faq question="Are categories still idiomatic Groovy?" >}} Yes, for DSLs and scoped extensions. For permanent extensions, traits or ExpandoMetaClass are preferred. {{< /faq >}}

{{< faq question="Do categories work with @CompileStatic?" >}} No. Categories are dynamic and require metaclass dispatch. Use @CompileDynamic on methods using categories. {{< /faq >}}

Mini Project

Create a logging category that adds logDebug, logInfo, and logError methods to all objects.

class LogCategory {
    static void logDebug(Object self, String msg) {
        println "[DEBUG] [${self.class.simpleName}] $msg"
    }

    static void logInfo(Object self, String msg) {
        println "[INFO] [${self.class.simpleName}] $msg"
    }

    static void logError(Object self, String msg) {
        println "[ERROR] [${self.class.simpleName}] $msg"
    }
}

use(LogCategory) {
    "test".logInfo("String operation")
    [1, 2, 3].logDebug("List iteration")
}

What's Next

Now that you understand categories, proceed to Groovy mixins.

Topic Description Link
Mixins Runtime behavior injection {{< ref "20-mixins" >}}
Traits Compile-time composition {{< ref "22-trait" >}}
Closures Anonymous functions {{< ref "04-closures" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro