Skip to content

Groovy Delegation — @Delegate and Method Forwarding Patterns

DodaTech Updated 2026-06-28 5 min read

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

Groovy's @Delegate annotation automatically forwards method calls to a delegate object, enabling composition-based design with minimal boilerplate code and transparent delegation.

What You'll Learn

  • Using @Delegate annotation
  • Delegation vs inheritance
  • Multiple delegates
  • Intercepting delegate methods

Why It Matters

Delegation enables composition over inheritance without writing tedious forwarding methods. DodaZIP uses delegation to compose compression codecs, forwarding format-specific operations to specialized handlers.

Real-World Use

Wrapper classes, decorator pattern, proxy implementations, and Adapter patterns where one class needs to expose another class's API transparently.

flowchart LR
    A["@Delegate"] --> B["Source Class"]
    B --> C["Delegate Object"]
    C --> D["Forwarded Calls"]
    A --> E["Multiple Delegates"]
    E --> F["Composition"]
    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

Basic @Delegate

import groovy.transform.Delegate

class EventLogger {
    void log(String msg) { println "[EVENT] $msg" }
    void error(String msg) { println "[ERROR] $msg" }
}

class Service {
    @Delegate EventLogger logger = new EventLogger()

    def process() {
        log "Processing started"  // Forwarded to EventLogger
        error "Something went wrong"
    }
}

def svc = new Service()
svc.process()
svc.log("Direct call")  // Also works

Without @Delegate, you would need to write forwarding methods manually.

Delegation vs Inheritance

// Inheritance approach
class BaseLogger {
    void log(String msg) { println msg }
}
class ServiceInheritance extends BaseLogger {}

// Delegation approach
class ServiceDelegation {
    @Delegate BaseLogger logger = new BaseLogger()
}

Delegation is more flexible because the delegate can be swapped at runtime and the class can still extend a different base.

Multiple Delegates

class FileOps {
    void read(String path) { println "Reading $path" }
    void write(String path, String data) { println "Writing to $path" }
}

class NetworkOps {
    void connect(String url) { println "Connecting to $url" }
    void send(String data) { println "Sending $data" }
}

class FileTransferService {
    @Delegate FileOps fileOps = new FileOps()
    @Delegate NetworkOps networkOps = new NetworkOps()

    def transfer(String url, String path) {
        connect(url)
        read(path)
        send("file content")
    }
}

The service exposes methods from both delegates.

Method Interception

Override delegate methods while still using delegation:

class AuditedLogger {
    @Delegate EventLogger logger = new EventLogger()

    void log(String msg) {
        logger.log("[AUDITED] $msg")
    }
}

def al = new AuditedLogger()
al.log("test")  // [AUDITED] test
al.error("fail") // Forwarded directly — [ERROR] fail

Delegate with Interfaces

interface Repository {
    void save(String key, Object value)
    Object find(String key)
}

class InMemoryRepo implements Repository {
    private store = [:]
    void save(String key, Object value) { store[key] = value }
    Object find(String key) { store[key] }
}

class CachedRepo {
    @Delegate Repository repo = new InMemoryRepo()
    private cache = [:]

    Object find(String key) {
        if (!cache.containsKey(key)) {
            cache[key] = repo.find(key)
        }
        cache[key]
    }
}

Changing Delegate at Runtime

class Switchable {
    @Delegate List<String> list = new ArrayList<>()

    void useSet() {
        list = new HashSet<>()
    }
}

def s = new Switchable()
s.add("a")
s.add("b")
println s.size()  // 2
s.useSet()
s.add("a")
s.add("a")         // Duplicate ignored in Set
println s.size()   // 1 (set behavior)

Delegate with @Delegate(interfaces=...)

Restrict which methods are forwarded:

interface Readable {
    String read()
}

interface Writable {
    void write(String data)
}

class ReadWriteDevice implements Readable, Writable {
    String read() { "data" }
    void write(String data) { println "Written: $data" }
}

class ReadOnlyView {
    @Delegate(interfaces=[Readable])
    ReadWriteDevice device = new ReadWriteDevice()
}

def view = new ReadOnlyView()
println view.read()  // data
// view.write("test")  // Not available — write is not forwarded

Common Mistakes

1. Forgetting to initialize the delegate

// Wrong — NullPointerException
class Bad {
    @Delegate List<String> list  // null!
}

Always initialize the delegate field.

2. Method conflicts between delegates

If two delegates have the same method, the first declared delegate wins. Order carefully.

3. Delegating to mutable objects unintentionally

The delegate is accessed via the field, not copied. Changes to the delegate affect the original.

4. Overriding all delegate methods defeats the purpose

If you override every method, you are not gaining from delegation. Only override what you need.

5. Circular delegation

A delegate pointing back to the enclosing class causes infinite Recursion.

Practice Questions

1. How does @Delegate reduce boilerplate?

It automatically generates forwarding methods for all public methods of the delegate type.

2. Can you have multiple @Delegate fields?

Yes, methods from all delegates are forwarded. Conflicts are resolved by declaration order.

3. How do you prevent specific methods from being delegated?

Use @Delegate(interfaces=[...]) to restrict forwarding to specific interfaces.

4. What happens when you override a delegated method?

Your override takes precedence. Other methods from the delegate are still forwarded.

Challenge: Use @Delegate to implement the Decorator pattern with multiple decorators wrapping a core component.

FAQ

{{< faq question="Does @Delegate work with Java classes?" >}} Yes. You can delegate to any Java class. The forwarding methods are generated at compile time. {{< /faq >}}

{{< faq question="Is @Delegate compile-time or runtime?" >}} Compile-time. The Groovy compiler generates forwarding methods during compilation. There is no runtime overhead beyond normal method calls. {{< /faq >}}

{{< faq question="Can I delegate to a null-safe proxy?" >}} Yes. Use the Elvis operator: @Delegate List<String> list = new ArrayList<>() ?: new LinkedList<>(). {{< /faq >}}

{{< faq question="Does @Delegate support generic delegates?" >}} Yes. Generic type parameters are preserved in the forwarded methods. {{< /faq >}}

{{< faq question="How do I intercept all delegate calls?" >}} Use Groovy's MOP (invokeMethod) or wrap the delegate in a Proxy/InvocationHandler. {{< /faq >}}

Mini Project

Create a Caching proxy using @Delegate that wraps a slow data source.

interface DataSource {
    String fetch(String key)
}

class SlowDatabase implements DataSource {
    String fetch(String key) {
        println "Slow fetch: $key"
        Thread.sleep(1000)
        "value_$key"
    }
}

class CachedDataSource {
    @Delegate DataSource source = new SlowDatabase()
    private cache = [:]

    String fetch(String key) {
        if (!cache.containsKey(key)) {
            cache[key] = source.fetch(key)
        }
        cache[key]
    }
}

def ds = new CachedDataSource()
println ds.fetch("a")  // Slow fetch, then cached
println ds.fetch("a")  // From cache — fast

What's Next

Now that you understand delegation, proceed to Groovy traits.

Topic Description Link
Traits Compile-time behavior {{< ref "22-trait" >}}
Mixins Runtime injection {{< ref "20-mixins" >}}
MOP Meta-Object Protocol {{< ref "18-meta-object-protocol" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro