Groovy Interfaces – Contracts and Polymorphism with 10 Tested Examples

Learn how Groovy interfaces work with 10 tested examples. Covers interface declaration, default methods, SAM types, closure coercion, and polymorphism on Groovy 5.x.

“Program to an interface, not an implementation. This is the single most important principle in object-oriented design.”

Gang of Four, Design Patterns

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate | Reading Time: 16 minutes

Interfaces are the backbone of polymorphism in object-oriented programming. They define a contract – a set of methods that a class promises to implement – without prescribing how those methods work internally. In Java, interfaces are strict and formal. In Groovy, they get superpowers. You can coerce closures into interfaces, use SAM (Single Abstract Method) types for concise functional code, and use Groovy’s dynamic nature to blur the line between compile-time contracts and runtime flexibility.

This post covers how to declare and implement Groovy interfaces, use default and static methods, work with SAM types and closure coercion, combine multiple interfaces, create marker interfaces, build interface hierarchies, and understand how interfaces interact with Groovy’s duck typing. We cover 10 tested examples from basic declarations through generics-based interfaces.

If you’re coming from the Groovy Classes and Inheritance post, interfaces are the natural next step. And if you’ve already explored Groovy Traits, you’ll see exactly how interfaces differ and when to choose one over the other.

What Are Interfaces in Groovy?

An interface in Groovy is a reference type that defines a contract – a set of method signatures that implementing classes must provide. Interfaces cannot be instantiated directly. Instead, classes declare that they implement an interface and then provide concrete implementations for each method defined in that interface.

According to the official Groovy documentation, Groovy interfaces follow Java’s interface model but add Groovy-specific capabilities like closure coercion and SAM type support. Starting with Java 8 compatibility, Groovy interfaces also support default and static methods.

Key Points:

  • Groovy interfaces use the interface keyword, just like Java
  • All methods in an interface are implicitly public and abstract (unless they are default or static)
  • Fields in interfaces are implicitly public static final (constants)
  • A class can implement multiple interfaces, solving the multiple-inheritance problem
  • Groovy can automatically coerce a closure into a SAM interface – no anonymous class needed

Why Use Interfaces?

Groovy is a dynamic language, so you might wonder why interfaces matter when duck typing already lets you call any method on any object. The answer is that interfaces provide structure, safety, and clarity that dynamic typing alone cannot offer:

  • Contracts – interfaces clearly define what a class must do, making APIs self-documenting
  • Polymorphism – you can treat different classes uniformly through a shared interface type
  • Testability – interfaces make it easy to create mock implementations for unit testing
  • @CompileStatic safety – when you enable static compilation with @CompileStatic, interfaces become enforced type contracts
  • Java interop – Groovy classes implementing Java interfaces integrate directly with Java frameworks like Spring, JUnit, and Hibernate
  • Design patterns – most design patterns rely on interfaces for loose coupling

Groovy Interfaces vs Java Interfaces

Groovy interfaces are compiled to standard Java interfaces on the JVM, so they are fully compatible with Java code. However, Groovy adds several conveniences on top:

  • Closure coercion – you can pass a closure where a SAM interface is expected, and Groovy converts it automatically
  • The as operator – you can coerce a closure or a map of closures into an interface implementation at runtime
  • Duck typing – in dynamic Groovy, you don’t strictly need an interface for polymorphism – if an object has the right methods, it works
  • Optional typing – you can use def instead of the interface type, and Groovy won’t complain at runtime
  • No checked exceptions – Groovy does not enforce checked exceptions, so interface methods don’t need throws clauses

The bottom line: Groovy interfaces work exactly like Java interfaces when you need them to, but Groovy gives you shortcuts that make working with them less verbose and more expressive.

10 Practical Examples

Let’s work through 10 practical, tested examples that cover every major aspect of Groovy interfaces. Each example is self-contained and runnable on Groovy 5.x with Java 17+.

Example 1: Basic Interface Declaration and Implementation

The most fundamental use of an interface – declare a contract and implement it in a class. Every implementing class must provide concrete implementations of all abstract methods declared in the interface.

BasicInterface.groovy

// Define an interface with two abstract methods
interface Greeter {
    String greet(String name)
    String farewell(String name)
}

// Implement the interface
class FormalGreeter implements Greeter {
    @Override
    String greet(String name) {
        return "Good day, ${name}."
    }

    @Override
    String farewell(String name) {
        return "Farewell, ${name}. Until we meet again."
    }
}

class CasualGreeter implements Greeter {
    @Override
    String greet(String name) {
        return "Hey, ${name}!"
    }

    @Override
    String farewell(String name) {
        return "Later, ${name}!"
    }
}

// Use polymorphism - treat both as Greeter
List<Greeter> greeters = [new FormalGreeter(), new CasualGreeter()]

greeters.each { Greeter g ->
    println g.greet("Alice")
    println g.farewell("Alice")
    println "---"
}

// Verify interface type
assert new FormalGreeter() instanceof Greeter
assert new CasualGreeter() instanceof Greeter
println "Both classes implement the Greeter interface"

Output

Good day, Alice.
Farewell, Alice. Until we meet again.
---
Hey, Alice!
Later, Alice!
---
Both classes implement the Greeter interface

The Greeter interface defines the contract. Both FormalGreeter and CasualGreeter implement it with different behavior. The calling code only needs to know about the Greeter type – it doesn’t care which concrete class is behind it. This is the essence of polymorphism.

Example 2: Multiple Interface Implementation

A class can implement multiple interfaces, gaining the contracts of each. This is how Groovy (and Java) solve the limitation of single inheritance – you extend one class but implement as many interfaces as you need.

MultipleInterfaces.groovy

interface Printable {
    String toPrintFormat()
}

interface Exportable {
    String toCSV()
    String toJSON()
}

interface Identifiable {
    String getId()
}

// A class implementing all three interfaces
class Product implements Printable, Exportable, Identifiable {
    String id
    String name
    double price

    @Override
    String toPrintFormat() {
        return "${name} (\$${price})"
    }

    @Override
    String toCSV() {
        return "${id},${name},${price}"
    }

    @Override
    String toJSON() {
        return """{"id":"${id}","name":"${name}","price":${price}}"""
    }

    @Override
    String getId() {
        return id
    }
}

def laptop = new Product(id: "P001", name: "Laptop", price: 999.99)

// Use as Printable
Printable printable = laptop
println "Print: ${printable.toPrintFormat()}"

// Use as Exportable
Exportable exportable = laptop
println "CSV: ${exportable.toCSV()}"
println "JSON: ${exportable.toJSON()}"

// Use as Identifiable
Identifiable identifiable = laptop
println "ID: ${identifiable.getId()}"

// Verify all interfaces
assert laptop instanceof Printable
assert laptop instanceof Exportable
assert laptop instanceof Identifiable
println "\nProduct implements all three interfaces"

Output

Print: Laptop ($999.99)
CSV: P001,Laptop,999.99
JSON: {"id":"P001","name":"Laptop","price":999.99}
ID: P001

Product implements all three interfaces

The Product class implements three separate interfaces. Each interface represents a different capability. The calling code can treat the same object through whichever interface lens it needs – printing, exporting, or identification. This is the power of multiple interface implementation.

Example 3: Default Methods (Java 8+ Style)

Since Groovy supports Java 8+ features, interfaces can contain default methods – methods with a concrete implementation that implementing classes inherit for free. This lets you add new methods to an interface without breaking existing implementations.

DefaultMethods.groovy

interface Logger {
    // Abstract method - must be implemented
    void log(String message)

    // Default method - inherited for free
    default void info(String message) {
        log("[INFO] ${message}")
    }

    default void warn(String message) {
        log("[WARN] ${message}")
    }

    default void error(String message) {
        log("[ERROR] ${message}")
    }

    // Default method with logic
    default void logWithTimestamp(String message) {
        def timestamp = new Date().format("yyyy-MM-dd HH:mm:ss")
        log("[${timestamp}] ${message}")
    }
}

// Only needs to implement the abstract method
class ConsoleLogger implements Logger {
    @Override
    void log(String message) {
        println message
    }
}

// Can override default methods if needed
class PrefixedLogger implements Logger {
    String prefix

    @Override
    void log(String message) {
        println "${prefix} >> ${message}"
    }

    // Override default behavior
    @Override
    void error(String message) {
        log("[CRITICAL ERROR] ${message}")
    }
}

println "=== ConsoleLogger ==="
def console = new ConsoleLogger()
console.info("Application started")
console.warn("Memory usage high")
console.error("Connection failed")

println "\n=== PrefixedLogger ==="
def prefixed = new PrefixedLogger(prefix: "APP")
prefixed.info("Application started")
prefixed.warn("Memory usage high")
prefixed.error("Connection failed")  // Uses overridden version

Output

=== ConsoleLogger ===
[INFO] Application started
[WARN] Memory usage high
[ERROR] Connection failed

=== PrefixedLogger ===
APP >> [INFO] Application started
APP >> [WARN] Memory usage high
APP >> [CRITICAL ERROR] Connection failed

The Logger interface defines one abstract method (log) and several default methods that build on it. ConsoleLogger only implements the abstract method and gets all defaults for free. PrefixedLogger overrides error() to customize its behavior. Default methods are a powerful way to evolve interfaces without breaking backward compatibility.

Example 4: Static Methods in Interfaces

Interfaces can also contain static methods – utility methods that belong to the interface itself, not to any implementing class. This is useful for factory methods or helper functions that relate to the interface’s purpose.

StaticMethods.groovy

interface Validator {
    boolean isValid(String input)
    String getErrorMessage()

    // Static factory method
    static Validator emailValidator() {
        return new Validator() {
            @Override
            boolean isValid(String input) {
                return input != null && input.matches(/^[\w.%+-]+@[\w.-]+\.\w{2,}$/)
            }

            @Override
            String getErrorMessage() {
                return "Invalid email format"
            }
        }
    }

    // Static factory method
    static Validator lengthValidator(int minLen, int maxLen) {
        return new Validator() {
            @Override
            boolean isValid(String input) {
                return input != null && input.length() >= minLen && input.length() <= maxLen
            }

            @Override
            String getErrorMessage() {
                return "Length must be between ${minLen} and ${maxLen} characters"
            }
        }
    }

    // Static utility method
    static boolean validateAll(String input, List<Validator> validators) {
        return validators.every { it.isValid(input) }
    }
}

// Use static factory methods
def emailCheck = Validator.emailValidator()
def lengthCheck = Validator.lengthValidator(5, 50)

// Test valid input
def testEmail = "alice@example.com"
println "Email '${testEmail}':"
println "  Email valid: ${emailCheck.isValid(testEmail)}"
println "  Length valid: ${lengthCheck.isValid(testEmail)}"
println "  All valid: ${Validator.validateAll(testEmail, [emailCheck, lengthCheck])}"

// Test invalid input
def badEmail = "abc"
println "\nEmail '${badEmail}':"
println "  Email valid: ${emailCheck.isValid(badEmail)}"
println "  Length valid: ${lengthCheck.isValid(badEmail)}"
println "  All valid: ${Validator.validateAll(badEmail, [emailCheck, lengthCheck])}"

// Show error messages for failures
[emailCheck, lengthCheck].each { v ->
    if (!v.isValid(badEmail)) {
        println "  Error: ${v.getErrorMessage()}"
    }
}

Output

Email 'alice@example.com':
  Email valid: true
  Length valid: true
  All valid: true

Email 'abc':
  Email valid: false
  Length valid: false
  All valid: false
  Error: Invalid email format
  Error: Length must be between 5 and 50 characters

Static methods on interfaces are perfect for factory methods. Instead of creating separate factory classes, you bundle the creation logic right inside the interface where it belongs. The Validator.emailValidator() and Validator.lengthValidator() calls read naturally, and the validateAll utility method provides a convenient way to apply multiple validators at once.

Example 5: SAM Type Interfaces with Closure Coercion

A SAM (Single Abstract Method) interface has exactly one abstract method. Groovy can automatically convert a closure into a SAM interface, which is one of Groovy’s most useful features for writing concise, functional-style code. This is also how functional interfaces work in Groovy.

SAMCoercion.groovy

// SAM interface - one abstract method
interface Transformer {
    String transform(String input)
}

// SAM interface with default methods still qualifies
interface Processor {
    String process(String input)

    default String processAll(List<String> inputs) {
        return inputs.collect { process(it) }.join(", ")
    }
}

// --- Traditional implementation ---
class UpperCaseTransformer implements Transformer {
    @Override
    String transform(String input) {
        return input.toUpperCase()
    }
}

// --- Closure coercion (automatic) ---
// When the target type is a SAM interface, Groovy auto-coerces
Transformer reverser = { String input -> input.reverse() }

// --- Using 'as' operator ---
def trimmer = { String input -> input.trim() } as Transformer

// --- Method accepting SAM type ---
String applyTransform(String text, Transformer t) {
    return t.transform(text)
}

println "Traditional: ${new UpperCaseTransformer().transform('hello')}"
println "Closure coercion: ${reverser.transform('hello')}"
println "As operator: ${trimmer.transform('  hello  ')}"

// Pass closures directly to methods expecting SAM interface
println "\nDirect closure passing:"
println applyTransform("hello world", { it.toUpperCase() })
println applyTransform("hello world", { it.capitalize() })
println applyTransform("hello world", { it.reverse() })

// SAM with default methods
Processor starWrapper = { String input -> "*** ${input} ***" }
println "\nSingle process: ${starWrapper.process('Groovy')}"
println "Process all: ${starWrapper.processAll(['Groovy', 'Java', 'Kotlin'])}"

// Verify types
assert reverser instanceof Transformer
assert starWrapper instanceof Processor
println "\nClosures successfully coerced to interface types"

Output

Traditional: HELLO
Closure coercion: olleh
As operator: hello

Direct closure passing:
HELLO WORLD
Hello world
dlrow olleh

Single process: *** Groovy ***
Process all: *** Groovy ***, *** Java ***, *** Kotlin ***

Closures successfully coerced to interface types

SAM coercion is very useful. Instead of writing verbose anonymous inner classes, you pass a closure and Groovy does the wrapping for you. This works automatically when the expected type is a SAM interface, or you can use the as operator for explicit coercion. Note that default methods don’t count toward the “single abstract method” – so Processor with one abstract and one default method is still a valid SAM interface.

Example 6: Interface as Method Parameter Type

One of the most practical uses of interfaces is as parameter types. This decouples your methods from specific implementations and makes your code flexible, testable, and extensible.

InterfaceParameter.groovy

interface DataSource {
    List<Map> fetchRecords()
    int getRecordCount()
}

interface Formatter {
    String format(List<Map> records)
}

// Concrete implementations
class InMemoryDataSource implements DataSource {
    List<Map> data

    @Override
    List<Map> fetchRecords() { return data }

    @Override
    int getRecordCount() { return data.size() }
}

class CSVFormatter implements Formatter {
    @Override
    String format(List<Map> records) {
        if (records.isEmpty()) return ""
        def headers = records[0].keySet().join(",")
        def rows = records.collect { r -> r.values().join(",") }
        return ([headers] + rows).join("\n")
    }
}

class TableFormatter implements Formatter {
    @Override
    String format(List<Map> records) {
        if (records.isEmpty()) return "(empty)"
        def headers = records[0].keySet().toList()
        def widths = headers.collect { h ->
            Math.max(h.length(), records.collect { it[h].toString().length() }.max())
        }
        def headerLine = headers.withIndex().collect { h, i -> h.padRight(widths[i]) }.join(" | ")
        def separator = widths.collect { "-" * it }.join("-+-")
        def rows = records.collect { r ->
            headers.withIndex().collect { h, i -> r[h].toString().padRight(widths[i]) }.join(" | ")
        }
        return ([headerLine, separator] + rows).join("\n")
    }
}

// Method that depends on interfaces, not implementations
String generateReport(DataSource source, Formatter formatter) {
    def records = source.fetchRecords()
    def count = source.getRecordCount()
    def formatted = formatter.format(records)
    return "Records: ${count}\n${formatted}"
}

// Create test data
def data = [
    [name: "Alice", role: "Developer", score: 95],
    [name: "Bob", role: "Designer", score: 87],
    [name: "Carol", role: "Manager", score: 92]
]

def source = new InMemoryDataSource(data: data)

println "=== CSV Report ==="
println generateReport(source, new CSVFormatter())

println "\n=== Table Report ==="
println generateReport(source, new TableFormatter())

Output

=== CSV Report ===
Records: 3
name,role,score
Alice,Developer,95
Bob,Designer,87
Carol,Manager,92

=== Table Report ===
Records: 3
name  | role      | score
------+-----------+------
Alice | Developer | 95
Bob   | Designer  | 87
Carol | Manager   | 92

The generateReport method doesn’t know or care about InMemoryDataSource, CSVFormatter, or TableFormatter. It only knows about the DataSource and Formatter interfaces. You could swap in a database data source, a file-based data source, or an XML formatter without changing a single line in generateReport. This is the Strategy Pattern in action, powered by interfaces.

Example 7: Marker Interfaces

A marker interface has no methods at all. Its sole purpose is to “mark” or “tag” a class as having a certain property or capability. You check for the marker using instanceof. Java’s Serializable and Cloneable are classic examples.

MarkerInterface.groovy

// Marker interfaces - no methods, just tags
interface Auditable {}
interface Cacheable {}
interface SoftDeletable {}

class User implements Auditable, SoftDeletable {
    String name
    String email
    boolean deleted = false

    String toString() { "User(${name})" }
}

class Config implements Cacheable {
    String key
    String value

    String toString() { "Config(${key}=${value})" }
}

class AuditLog implements Auditable, Cacheable {
    String action
    Date timestamp = new Date()

    String toString() { "AuditLog(${action})" }
}

// Processing based on marker interfaces
void processEntity(Object entity) {
    println "Processing: ${entity}"

    if (entity instanceof Auditable) {
        println "  -> Writing audit trail for ${entity.class.simpleName}"
    }

    if (entity instanceof Cacheable) {
        println "  -> Adding to cache: ${entity.class.simpleName}"
    }

    if (entity instanceof SoftDeletable) {
        println "  -> Supports soft delete: ${entity.class.simpleName}"
    }

    println ""
}

// Test with different entities
def entities = [
    new User(name: "Alice", email: "alice@test.com"),
    new Config(key: "theme", value: "dark"),
    new AuditLog(action: "user.login")
]

entities.each { processEntity(it) }

// Filter by marker
def auditableItems = entities.findAll { it instanceof Auditable }
println "Auditable entities: ${auditableItems}"

def cacheableItems = entities.findAll { it instanceof Cacheable }
println "Cacheable entities: ${cacheableItems}"

Output

Processing: User(Alice)
  -> Writing audit trail for User
  -> Supports soft delete: User

Processing: Config(theme=dark)
  -> Adding to cache: Config

Processing: AuditLog(user.login)
  -> Writing audit trail for AuditLog
  -> Adding to cache: AuditLog

Auditable entities: [User(Alice), AuditLog(user.login)]
Cacheable entities: [Config(theme=dark), AuditLog(user.login)]

Marker interfaces are simple but effective. They let you categorize classes at compile time and branch logic at runtime using instanceof. Modern frameworks like Spring use a similar approach (though annotations have largely replaced marker interfaces in new code). In Groovy, marker interfaces remain useful for type-safe categorization, especially when working with @CompileStatic.

Example 8: Interface Inheritance (extends)

Interfaces can extend other interfaces using extends, creating a hierarchy of increasingly specific contracts. Unlike classes, an interface can extend multiple parent interfaces.

InterfaceInheritance.groovy

// Base interfaces
interface Readable {
    String read()
}

interface Writable {
    void write(String data)
}

interface Closeable {
    void close()
}

// Compound interface - extends multiple interfaces
interface ReadWriteStream extends Readable, Writable, Closeable {
    long getPosition()
    void seek(long position)
}

// Another compound: read-only stream
interface ReadOnlyStream extends Readable, Closeable {
    long size()
}

// Implement the compound interface
class StringStream implements ReadWriteStream {
    StringBuilder buffer = new StringBuilder()
    long position = 0

    @Override
    String read() {
        if (position >= buffer.length()) return ""
        def result = buffer.substring((int) position)
        position = buffer.length()
        return result
    }

    @Override
    void write(String data) {
        buffer.append(data)
    }

    @Override
    void close() {
        println "Stream closed. Final content: '${buffer}'"
    }

    @Override
    long getPosition() {
        return position
    }

    @Override
    void seek(long pos) {
        this.position = Math.min(pos, buffer.length())
    }
}

// Use the compound interface
ReadWriteStream stream = new StringStream()
stream.write("Hello, ")
stream.write("Groovy ")
stream.write("World!")

println "Position before read: ${stream.getPosition()}"
stream.seek(0)
println "After seek to 0: ${stream.read()}"
println "Position after read: ${stream.getPosition()}"

stream.close()

// Verify the hierarchy
def s = new StringStream()
assert s instanceof ReadWriteStream
assert s instanceof Readable
assert s instanceof Writable
assert s instanceof Closeable
println "\nStringStream implements all parent interfaces"

// Show interface hierarchy
println "\nReadWriteStream extends:"
ReadWriteStream.interfaces.each { println "  - ${it.simpleName}" }

Output

Position before read: 0
After seek to 0: Hello, Groovy World!
Position after read: 20
Stream closed. Final content: 'Hello, Groovy World!'

StringStream implements all parent interfaces

ReadWriteStream extends:
  - Readable
  - Writable
  - Closeable

Interface inheritance lets you build composite contracts from smaller, focused interfaces. The ReadWriteStream interface extends Readable, Writable, and Closeable, combining all their contracts plus adding its own methods. A class implementing ReadWriteStream automatically satisfies all parent interfaces – you can pass it anywhere a Readable, Writable, or Closeable is expected. This follows the Interface Segregation Principle from SOLID design.

Example 9: Groovy Duck Typing vs Interfaces

Groovy’s dynamic nature means you don’t always need formal interfaces for polymorphism. If an object has the right methods, it works – regardless of its declared type. This is called duck typing: “If it walks like a duck and quacks like a duck, it’s a duck.” Let’s compare both approaches.

DuckTypingVsInterface.groovy

// --- Approach 1: Formal Interface ---
interface Speaker {
    String speak()
}

class Dog implements Speaker {
    String name
    @Override
    String speak() { "Woof! I'm ${name}" }
}

class Cat implements Speaker {
    String name
    @Override
    String speak() { "Meow! I'm ${name}" }
}

void makeSpeak(Speaker s) {
    println s.speak()
}

println "=== With Interfaces ==="
makeSpeak(new Dog(name: "Rex"))
makeSpeak(new Cat(name: "Whiskers"))

// --- Approach 2: Duck Typing (no interface) ---
class Duck {
    String name
    String speak() { "Quack! I'm ${name}" }
}

class Parrot {
    String name
    String speak() { "Polly wants a cracker! I'm ${name}" }
}

// No interface needed - just call the method
void makeSpeakDynamic(thing) {
    println thing.speak()
}

println "\n=== With Duck Typing ==="
makeSpeakDynamic(new Duck(name: "Donald"))
makeSpeakDynamic(new Parrot(name: "Polly"))
makeSpeakDynamic(new Dog(name: "Rex"))  // Works too!

// --- When duck typing fails ---
class Rock {
    String name
    // No speak() method!
}

println "\n=== Duck Typing Failure ==="
try {
    makeSpeakDynamic(new Rock(name: "Boulder"))
} catch (MissingMethodException e) {
    println "Error: ${e.message.split('\n')[0]}"
}

// --- When to use each approach ---
println "\n=== Comparison ==="
println "Interfaces: Compile-time safety, IDE support, clear API contracts"
println "Duck typing: Flexible, less boilerplate, works across unrelated classes"
println "Best practice: Use interfaces for public APIs, duck typing for scripts"

Output

=== With Interfaces ===
Woof! I'm Rex
Meow! I'm Whiskers

=== With Duck Typing ===
Quack! I'm Donald
Polly wants a cracker! I'm Polly
Woof! I'm Rex

=== Duck Typing Failure ===
Error: No signature of method: Rock.speak() is applicable for argument types: () values: []

=== Comparison ===
Interfaces: Compile-time safety, IDE support, clear API contracts
Duck typing: Flexible, less boilerplate, works across unrelated classes
Best practice: Use interfaces for public APIs, duck typing for scripts

Duck typing is powerful but risky. The Rock class doesn’t have a speak() method, and you only discover this at runtime. With interfaces, the compiler catches this mistake immediately. The rule of thumb: use interfaces for library code, public APIs, and anything that benefits from type checking. Use duck typing for quick scripts, prototypes, and internal code where flexibility trumps safety.

Example 10: Interfaces with Generics

Interfaces can use generics to create type-safe contracts that work with any type. This combines the power of polymorphism with type safety, and it’s the foundation of patterns like Repository, Converter, and Comparable.

GenericInterface.groovy

// Generic interface with type parameter
interface Repository<T> {
    void save(T entity)
    T findById(String id)
    List<T> findAll()
    void delete(String id)
}

// Generic converter interface with two type parameters
interface Converter<F, T> {
    T convert(F source)
}

// Entity classes
class User {
    String id
    String name
    String email
    String toString() { "User(${id}: ${name})" }
}

class Product {
    String id
    String name
    double price
    String toString() { "Product(${id}: ${name} @ \$${price})" }
}

// Implement Repository for User
class UserRepository implements Repository<User> {
    Map<String, User> store = [:]

    @Override
    void save(User entity) {
        store[entity.id] = entity
        println "Saved: ${entity}"
    }

    @Override
    User findById(String id) { store[id] }

    @Override
    List<User> findAll() { store.values().toList() }

    @Override
    void delete(String id) {
        def removed = store.remove(id)
        println "Deleted: ${removed}"
    }
}

// Implement Repository for Product
class ProductRepository implements Repository<Product> {
    Map<String, Product> store = [:]

    @Override
    void save(Product entity) {
        store[entity.id] = entity
        println "Saved: ${entity}"
    }

    @Override
    Product findById(String id) { store[id] }

    @Override
    List<Product> findAll() { store.values().toList() }

    @Override
    void delete(String id) {
        def removed = store.remove(id)
        println "Deleted: ${removed}"
    }
}

// Implement Converter
class UserToMapConverter implements Converter<User, Map> {
    @Override
    Map convert(User source) {
        return [id: source.id, name: source.name, email: source.email]
    }
}

// --- Test Repository pattern ---
println "=== User Repository ==="
Repository<User> userRepo = new UserRepository()
userRepo.save(new User(id: "U1", name: "Alice", email: "alice@test.com"))
userRepo.save(new User(id: "U2", name: "Bob", email: "bob@test.com"))
println "Find U1: ${userRepo.findById('U1')}"
println "All users: ${userRepo.findAll()}"
userRepo.delete("U2")

println "\n=== Product Repository ==="
Repository<Product> productRepo = new ProductRepository()
productRepo.save(new Product(id: "P1", name: "Laptop", price: 999.99))
productRepo.save(new Product(id: "P2", name: "Mouse", price: 29.99))
println "All products: ${productRepo.findAll()}"

println "\n=== Converter ==="
Converter<User, Map> converter = new UserToMapConverter()
def userMap = converter.convert(new User(id: "U1", name: "Alice", email: "alice@test.com"))
println "User as Map: ${userMap}"

// SAM coercion with generic interface
Converter<String, Integer> strToInt = { String s -> s.toInteger() }
println "Converted '42': ${strToInt.convert('42')}"

Converter<String, String> upperCase = { it.toUpperCase() }
println "Converted 'hello': ${upperCase.convert('hello')}"

Output

=== User Repository ===
Saved: User(U1: Alice)
Saved: User(U2: Bob)
Find U1: User(U1: Alice)
All users: [User(U1: Alice), User(U2: Bob)]
Deleted: User(U2: Bob)

=== Product Repository ===
Saved: Product(P1: Laptop @ $999.99)
Saved: Product(P2: Mouse @ $29.99)
All products: [Product(P1: Laptop @ $999.99), Product(P2: Mouse @ $29.99)]

=== Converter ===
User as Map: [id:U1, name:Alice, email:alice@test.com]
Converted '42': 42
Converted 'hello': HELLO

Generic interfaces are the backbone of enterprise patterns. The Repository<T> interface defines CRUD operations that work with any entity type. The Converter<F, T> interface transforms objects between types. Notice how SAM coercion works with generic interfaces too – the last two converters are just closures coerced into Converter instances. For deep coverage of generics, see the Groovy Generics guide.

Interfaces vs Traits vs Abstract Classes

Groovy gives you three tools for defining contracts and sharing behavior. Choosing the right one matters. Here’s a practical comparison:

Interfaces:

  • Define pure contracts (method signatures)
  • Can have default and static methods (Java 8+)
  • Cannot hold instance state (no mutable fields)
  • A class can implement multiple interfaces
  • Best for: API contracts, type markers, Java interop, SAM/functional types

Traits:

  • Can contain concrete method implementations and fields with state
  • Support conflict resolution when multiple traits define the same method
  • Can be applied at runtime using withTraits()
  • A class can implement multiple traits
  • Best for: reusable behavior, cross-cutting concerns, composition over inheritance

Abstract Classes:

  • Can have constructors, instance state, and both abstract and concrete methods
  • Subject to single inheritance – a class can extend only one abstract class
  • Can have access modifiers on methods (protected, private)
  • Best for: partial implementations with shared state, template method pattern

Quick decision guide: If you need a pure contract with no shared logic, use an interface. If you need reusable behavior that multiple unrelated classes can share, use a trait. If you need a base class with constructors, shared state, and a common ancestor, use an abstract class. In practice, you’ll often combine all three – abstract classes that implement interfaces and mix in traits.

Best Practices

Here are tested guidelines for working with Groovy interfaces effectively:

  • Keep interfaces small and focused – follow the Interface Segregation Principle. An interface with 15 methods is hard to implement. Split it into smaller, cohesive interfaces and compose them through inheritance if needed.
  • Use SAM interfaces for callback patterns – if your interface has a single purpose, make it a SAM type so callers can pass closures directly. This dramatically reduces boilerplate.
  • Prefer interfaces over concrete types in method signatures – accept List instead of ArrayList, DataSource instead of MySQLDataSource. This keeps your methods flexible and testable.
  • Use default methods sparingly – they are great for backward-compatible evolution of interfaces, but don’t turn your interface into a trait. If you need significant shared logic, use a trait instead.
  • Name interfaces after capabilities, not thingsPrintable, Cacheable, Sortable are better names than Printer, Cache, Sorter because they describe what an object can do, not what it is.
  • Consider @CompileStatic for interface-heavy code – when you use interfaces as types, enabling static compilation catches type mismatches at compile time rather than runtime.
  • Use generic interfaces for reusable patterns – the Repository, Converter, Comparator, and Factory patterns all benefit from generic interfaces that work with any type while maintaining type safety.
  • Document the contract – add Groovydoc comments to your interface methods explaining what implementors are expected to do, what the parameters mean, and what the return value represents.

Conclusion

Groovy interfaces give you the best of both worlds: Java’s compile-time contracts and Groovy’s dynamic expressiveness. You can declare strict API boundaries with interfaces, then use closure coercion and SAM types to implement them with minimal ceremony. Default methods let interfaces evolve without breaking existing code. Interface inheritance lets you compose contracts from smaller building blocks. And when you combine interfaces with traits and generics, you have a solid foundation for designing flexible, maintainable, and type-safe Groovy applications.

The 10 examples in this post covered the full spectrum – from basic declarations through generic Repository patterns. The bottom line: program to interfaces, not implementations. Your code will be more testable, more flexible, and easier to extend. Interfaces apply equally to libraries, web applications, and Groovy scripts – they’re a tool you’ll reach for again and again.

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 Constructors and Named Parameters

Frequently Asked Questions

What is an interface in Groovy?

A Groovy interface is a reference type that defines a contract – a set of method signatures that any implementing class must provide. Interfaces support default methods (with concrete implementation), static methods, and constants. They are compiled to standard Java interfaces on the JVM and are fully interoperable with Java code.

Can a Groovy class implement multiple interfaces?

Yes. A Groovy class can implement as many interfaces as needed using the implements keyword: class MyClass implements InterfaceA, InterfaceB, InterfaceC. The class must provide concrete implementations for all abstract methods from every interface. This is how Groovy (and Java) achieve a form of multiple inheritance.

What is SAM type coercion in Groovy interfaces?

SAM stands for Single Abstract Method. When a Groovy interface has exactly one abstract method, Groovy can automatically convert a closure into an implementation of that interface. For example, Comparator comp = { a, b -> a <=> b } works because Comparator has a single abstract method (compare). This eliminates the need for verbose anonymous inner classes.

What is the difference between a Groovy interface and a trait?

Interfaces define contracts (method signatures) while traits provide reusable behavior (concrete implementations with state). Interfaces cannot hold instance state, but traits can have fields. Both support multiple implementation. Use interfaces for API contracts and Java interop; use traits when you need to share concrete behavior across unrelated classes.

Should I use interfaces or duck typing in Groovy?

Use interfaces for public APIs, library code, and projects using @CompileStatic – they provide compile-time safety, IDE support, and clear documentation. Use duck typing for quick scripts, prototypes, and internal code where flexibility is more important than type safety. In practice, most production Groovy code benefits from interfaces.

Previous in Series: Groovy Classes and Inheritance

Next in Series: Groovy Constructors and Named Parameters

Related Topics You Might Like:

This post is part of the Groovy 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 *