GEP-22


Metadata
Number

GEP-22

Title

Traits

Version

1

Type

Feature

Status

Final

Comment

Delivered in Groovy 2.3 and refined in later versions; static-member support remains incubating

Leader

Cédric Champeau

Created

2026-05-06

Last modification

2026-05-06

Abstract: Traits

Traits are a structural construct of the Groovy language that allow:

  • composition of behaviour

  • runtime implementation of interfaces

  • behaviour overriding

  • compatibility with static type checking and static compilation

Conceptually a trait is an interface that may carry both default implementations and state. A class declares its participation in a trait via the implements clause exactly as it would for any other interface. Traits compose into the implementing class at compile time (or, optionally, at runtime via coercion) without requiring multiple inheritance of classes.

This GEP specifies the language semantics of traits, captures the bytecode compilation model, and records the evolution of the feature across Groovy releases. Worked examples and tutorial-style code samples live in the language specification (_traits.adoc); this document is intentionally terse and prescriptive.

Motivation

Object-oriented codebases routinely need to reuse behaviour across class hierarchies that cannot share a common ancestor. The available pre-existing mechanisms each carry trade-offs:

  • Single inheritance forces all reuse through a single line, leading to deep hierarchies, fragile base-class problems, and inability to mix unrelated capabilities.

  • Java 8 default methods attach behaviour to interfaces but are stateless by design, do not support stackable composition through super, and their resolution rules are designed around binary compatibility rather than composition.

  • Runtime mixins (Groovy’s pre-2.3 @Mixin annotation, since deprecated) weave behaviour at runtime but the resulting object is not an instanceof the mixed-in type, defeating type-based dispatch and compile-time checks.

  • Delegation via @Delegate expresses has-a relationships well but is verbose for genuine is-a composition and does not support overriding delegated behaviour transparently.

Traits address these gaps. Drawing on the work of Schärli, Ducasse, Black and Nierstrasz (ECOOP 2003), and on Scala’s analogous construct, a trait combines the contract role of an interface with the reusable-implementation role of a class while imposing deterministic composition rules. As Cédric Champeau put it when introducing them: traits extend the benefit of interfaces to concrete classes without producing inheritance pyramids, allowing new APIs to be layered onto existing classes without modification.

Specification

Declaration

A trait is declared with the trait keyword:

trait FlyingAbility {
    String fly() { "I'm flying!" }
}

The annotation @groovy.transform.Trait is an exact synonym and may be applied to any class declaration that would otherwise satisfy the trait shape; the AST transform produces equivalent output. The trait keyword is preferred in source.

A trait may declare any of: methods (public or private, abstract or concrete, static or instance), properties, fields (public or private, instance or static), and an implements clause naming interfaces and/or super-traits. A trait may extend at most one super-trait via extends, or any number of super-traits by listing them in implements.

Constructors are not permitted on traits.

Methods

  • Public and private methods are supported. protected and package-private are not supported.

  • Abstract methods are permitted; concrete classes that implement the trait must provide an implementation unless they too are abstract.

  • Private methods do not appear on the generated trait interface and are not visible to other classes.

  • The final modifier records the intended modifier of the woven method in the implementing class. Mixing final and non-final declarations of the same signature across multiple inherited traits is permitted; normal trait method-selection rules apply and the resulting method takes the modifier of the selected source.

Fields

Fields declared in a trait are not stored on the generated trait interface (interfaces cannot hold instance state). Instead they are stored on a synthetic field helper class and woven into the implementing class under name-mangled identifiers:

  • For a field bar of type T declared in trait my.pkg.Foo, the implementing class gains a field named my_pkg_Foo__bar of type T.

  • The mangling rule is: replace each . in the package name with , append <TraitSimpleName>__<fieldName>. The double underscore separates the trait identity from the field identity.

  • Private fields follow the same scheme but are also marked private; the mangling guarantees no collisions when a class implements multiple traits that declare fields of the same name (the diamond problem for state).

Use of public fields in traits is discouraged in favour of properties.

Properties

A property declared in a trait yields the usual auto-generated accessor pair (getX/setX, or isX for boolean) on the generated trait interface, and a backing field on the implementing class woven via the field-helper mechanism. Property semantics are otherwise identical to those of a regular Groovy class.

this, super, and stackable traits

Inside a trait method:

  1. this refers to the implementing instance, never to a trait-level artefact. A trait should be reasoned about as if it were a superclass of the implementing class, despite the fact that no such class exists in the runtime hierarchy.

  2. An unqualified super.m(…​) call delegates to the next trait in the implementation chain (in the order produced by the implements clause of the implementing class, walked right-to-left). When the chain is exhausted, super resolves to the actual superclass of the implementing class.

  3. A qualified T.super.m(…​) call resolves to trait T’s implementation of `m, regardless of position in the chain. This is the explicit override form.

This rule set yields stackable traits: behaviours that delegate to the next trait via unqualified super can be composed in different orders to produce different overall behaviours, without those traits needing to know about each other.

Multiple inheritance and conflict resolution

When a class implements two or more traits that supply conflicting implementations of a method with the same signature, the last trait listed in the implements clause wins. This is the deterministic default. The implementing class may override the resolution:

  • by providing its own implementation of the method, or

  • by calling T.super.m(…​) for the desired trait T.

The same rule applies to property accessors and field-helper-backed state. Diamond conflicts on field state cannot arise: each trait’s fields live under their own mangled names.

SAM coercion

A trait that declares exactly one abstract method is a Single Abstract Method type for the purposes of closure coercion. A Closure may be assigned or coerced to such a trait, in which case the closure body becomes the implementation of the abstract method. SAM coercion of traits composes with the rules in GEP-12.

Runtime application

Traits may be applied to an existing object at runtime:

def proxy = subject as SomeTrait
def proxy = subject.withTraits(TraitA, TraitB)

Both forms produce a new proxy instance — never the original object — that:

  • implements SomeTrait (or TraitA and TraitB),

  • implements every interface that the original object implemented, and

  • delegates to the original object for behaviour not supplied by the applied trait(s).

A trait method always takes precedence over the corresponding method on the proxied object when both exist. The proxy is not an instance of the original class; consumers that rely on instanceof against the original concrete class must continue to use the underlying object.

Static members

Static methods, properties and fields in a trait are supported with the following constraints (collectively flagged incubating / experimental):

  • Each implementing class receives its own copy of static members. Static state is not shared across implementing classes — a static field declared in a trait is conceptually a template, instantiated per implementer.

  • Static members are not exposed on the generated trait interface. Calls of the form Trait.staticMethod(…​) are not statically valid.

  • Static members are accessed dynamically and are not subject to static type checking; traits with static methods cannot be @CompileStatic.

  • Mixing static and instance methods of the same signature across multiple traits is undefined behaviour and should be avoided. If selection chooses a static variant where an instance variant is required, a compilation error is raised; conversely, an instance selection silently shadows the static variant.

@SelfType

@groovy.transform.SelfType declares a list of types that any class implementing the annotated trait must extend or implement. The compiler verifies the constraint at compile time and reports a clear error if it is violated. Self types make traits that depend on the surrounding class’s API safe to type-check and statically compile, without resorting to explicit (this as SomeBaseClass) casts inside trait method bodies.

Interaction with @Sealed

Traits may be sealed (see GEP-13). @Sealed and @SelfType are orthogonal:

  • @SelfType constrains what an implementing class must already be (its supertype shape).

  • @Sealed constrains which specific classes are allowed to implement the trait at all.

A trait may carry both annotations. For the degenerate single-permitted- implementer case, @SelfType(Foo) is preferred over @Sealed(permittedSubclasses = ['Foo']) for clarity.

Bytecode and stub model

A trait pkg.T produces, after compilation, the following artefacts:

  1. An interface pkg.T containing the abstract and public-method signatures, plus accessor signatures for any properties. This is the type that participates in instanceof, generics, and Java interop. The interface contains no default methods — trait method bodies are not bytecode-default-method bodies, even on JDK 8+.

  2. A trait helper class named pkg.T$Trait$Helper (constant Traits.TRAIT_HELPER). Each non-abstract trait method m(args) is emitted as a static method on the helper, taking the receiver as an explicit first parameter (and threading any captured trait state through field-helper accessors). Bridge methods are woven into each implementing class to forward instance calls to the helper.

  3. A field helper class named pkg.T$Trait$FieldHelper (constant Traits.FIELD_HELPER). It exposes get/set accessors for trait instance fields, parameterised by the receiver. The implementing class is given mangled fields and small bridge accessors that delegate to this helper.

  4. A static field helper class named pkg.T$Trait$StaticFieldHelper (constant Traits.STATIC_FIELD_HELPER) when the trait declares static fields, providing per-implementer storage for static state.

This four-class model is what makes traits visible to Java callers as plain interfaces (with no default methods to surprise Java consumers), while still supplying state, stackable super, and conflict resolution that a Java-default-method approach cannot.

Limitations

  • AST transform compatibility is best-effort. Some transforms (@CompileStatic, @TypeChecked, logging transforms) apply cleanly to traits; others apply only to the implementing class; others are unsupported. New transforms should be evaluated case by case.

  • Prefix and postfix operators (+`, `--`) on a trait field are rejected at compile time. The workaround is `= / -=. The reason is that the field-helper indirection cannot represent the read-modify-write sequence atomically.

  • Constructors on traits are not supported. Initialisation logic should be expressed via abstract methods supplied by the implementing class, or via property defaults.

  • Inheritance-of-state gotcha: trait method bodies that read a trait field directly read the trait’s own field, not any same-named property declared by the implementing class. To pick up an overriding value, trait code should access state through accessors (getX()), not by direct field reference.

Cross-version evolution

Version Year Change

2.3.0

2014

Initial release of traits. trait keyword and @Trait annotation introduced; public/private methods; abstract methods; instance fields with name mangling; properties; multiple inheritance with last-wins conflict resolution; explicit T.super.m() resolution; stackable unqualified super; runtime application via as and Object.withTraits(…​); SAM coercion of single-abstract-method traits.

2.4.0

2015

@SelfType annotation introduced (GROOVY-7134), enabling statically checked traits whose method bodies depend on members supplied by the implementing class’s superclass hierarchy.

3.0

2020

Default methods declared in interface types using the Java 8 syntax were accepted by the parser and implemented under the hood by delegating to the trait machinery (incubating). No spec-level change to trait semantics.

4.0

2022

Sealed traits supported via @Sealed / the sealed keyword and the permits clause (see GEP-13). Distinction between @Sealed and @SelfType formalised in the spec. Java stub generation for static trait properties hardened.

5.0

2024

Default, private and static methods in interface types are now implemented as native JVM bytecode rather than via the trait machinery (GROOVY-8299). This decouples the interface-default-method feature from traits and improves Java interop. Multiple bug fixes landed for trait + @TupleConstructor default-value handling (GROOVY-8219, GROOVY-8788) and for trait static-field generation under static compilation (GROOVY-11817, GROOVY-11907).

6.0+

TBD

No spec-level changes to traits planned at the time of writing. This GEP is the canonical location to record any future change.

Non-goals and potential future extensions

The following items are deliberately out of scope for this GEP and for the current implementation. Any of them would warrant a follow-up revision of this document or a successor GEP.

  • Promoting static-member support out of incubating status. The current per-implementer template semantics is a deliberate JVM-shaped compromise; a "true" shared-static-member model would require either breaking that semantics or moving static state into a separate runtime holder.

  • Emitting trait methods as native interface default methods on JDK 8+. The four-class helper model deliberately avoids this so that Java callers see traits as plain interfaces. Switching would be a Java-interop change.

  • First-class compatibility guarantees for arbitrary AST transforms on traits.

  • Constructor support on traits.

  • protected or package-private trait methods.

  • Schärli, Ducasse, Nierstrasz, Black. Traits: Composable Units of Behaviour. ECOOP 2003. — the academic foundation.

  • Scala traits — the closest sibling construct in another JVM language.

  • JEP 126: Default Methods — Java’s stateless analogue, contrasted in the Motivation section.

  • GEP-12: SAM coercion — underlies trait SAM coercion.

  • GEP-13: Sealed classes — applies to traits (sealed traits).

  • Cédric Champeau, Rethinking API design with traits — original design motivation and worked examples.

  • The language specification chapter on traits in the Groovy documentation contains worked tutorial examples that complement this spec-only document.

Reference implementation

Package: org.codehaus.groovy.transform.trait

  • TraitASTTransformation — entry point invoked for every trait declaration; produces the interface, helper, field-helper and (where applicable) static-field-helper artefacts.

  • TraitComposer — weaves trait methods, accessors and bridge methods into each implementing class.

  • TraitReceiverTransformer — rewrites references inside trait method bodies so that this, super and field accesses resolve to the implementing-instance receiver and to helper-routed state.

  • Traits — utility class holding helper-class naming constants ($Trait$Helper, $Trait$FieldHelper, $Trait$StaticFieldHelper), trait detection predicates, and self-type collection logic.

  • TraitTypeCheckingExtension — integrates traits with the static type checker (GEP-8).

Public API:

  • groovy.transform.Trait (since 2.3.0)

  • groovy.transform.SelfType (since 2.4.0)

Representative JIRA issues

  • GROOVY-7134: introduce @SelfType for statically checked traits.

  • GROOVY-8233: Java stub generation for static properties of traits.

  • GROOVY-8299: native default/private/static methods in interfaces (decoupling from trait machinery in 5.0).

  • GROOVY-8219, GROOVY-8788: @TupleConstructor interaction with traits and default values.

  • GROOVY-8951: trait getter conflicts with generated getter (pre-compiled case).

  • GROOVY-11674: deterministic ordering of trait methods for reproducible builds.

  • GROOVY-11907: trait field reference transform restructure.

Update history

1 (2026-05-06) Initial draft. Retrospective specification capturing trait semantics as shipped in 2.3.0 and refined through 5.0, the four-class bytecode model, and the cross-version evolution table.