Trading Flexibility for Speed: Using @CompileStatic in Groovy
How @CompileStatic changes Groovy's type checking, dispatch, and tooling, with a worked example, verification steps, and the trade-offs it brings.
08 Oct 2026, 06:08 UTC

The Cost of Dynamic Dispatch
Groovy is dynamically typed by default. When you call a method or read a property, the call is resolved at runtime through the MetaObject Protocol (MOP), Groovy's mechanism for intercepting and dispatching calls. That flexibility powers DSLs and runtime metaprogramming, but it has two practical costs: a typo like user.emial compiles fine and only surfaces at runtime as a MissingPropertyException, and every call goes through Groovy's call-site caching layer instead of a direct JVM invocation.
If you are writing a hot loop, a numeric routine, or a service where correctness matters more than metaprogramming, @CompileStatic (from groovy.transform) tells the compiler to type-check the code and emit Java-like bytecode with direct dispatch.
What Changes When You Annotate
Type Checking Moves to Compile Time
With the annotation, the compiler resolves every method and property against declared types during the build. The misspelled property becomes a compilation error instead of a production exception. IDE completions and refactoring also behave closer to what you would expect in Java.
Dispatch Bypasses the Call-Site Layer
Dynamic Groovy routes calls through generated call-site machinery that caches lookups. Statically compiled code uses ordinary JVM instructions such as invokevirtual instead. The difference is typically largest in tight loops and numeric code; for I/O-bound code, expect little measurable change. Treat performance as typical, not guaranteed — only a benchmark of your own workload, for example with JMH, proves a gain for your code on your JDK and Groovy version.
A Worked Example
Consider a service that totals transaction records. In dynamic Groovy, a misspelled property inside the loop compiles and can fail partway through a batch.
import groovy.transform.CompileStatic
class Transaction {
String id
double amount
}
@CompileStatic
class TransactionProcessor {
double calculateTotal(List<Transaction> transactions) {
double total = 0
for (Transaction tx : transactions) {
// A typo like tx.amunt fails compilation here, not at runtime
total += tx.amount
}
return total
}
}
The annotation can be applied to a whole class or to individual methods, so a codebase can stay mostly dynamic while the performance- and correctness-critical parts are locked down.
Checking the Result Yourself
- Compile the class twice, once with and once without the annotation, introducing a deliberate typo such as
tx.amunt. Only the annotated variant should fail to compile. Exact diagnostics vary by Groovy version, so confirm against the documentation for the version in your build. - Inspect the bytecode. From the directory holding compiled classes (for example
build/classes/groovy/mainin a Gradle build), runjavap -c TransactionProcessor. No special permissions are needed. The dynamic variant references Groovy's call-site runtime classes; the statically compiled variant callsTransaction.getAmount()directly. - If performance is the motivation, measure before and after with a microbenchmark rather than assuming the win.
The Trade-offs
Static compilation disables the features that rely on runtime dispatch:
methodMissingandpropertyMissingcannot be intercepted by statically compiled callers.- Runtime metaclass changes are invisible to statically compiled code.
- Some closure coercion patterns and untyped idioms need explicit types or casts. Groovy's inference is not identical to Java's, and generics and closure inference have improved across versions, so diagnostics are version-sensitive.
If you want the compile-time checking but still need dynamic dispatch semantics, @TypeChecked performs the type checking without changing how calls are dispatched.
| Code | Recommendation | Reason |
|---|---|---|
| Gradle Groovy build scripts | Stay dynamic | Build DSLs rely on runtime property resolution; blanket static compilation is impractical. |
| Domain models and hot service methods | @CompileStatic | Earlier error detection and direct dispatch where it pays off. |
| Code using runtime mocks or metaprogramming-heavy libraries | Check first | Libraries that patch metaclasses at runtime can fail in surprising ways under static compilation. |
Where to Start
Do not annotate the whole project. Pick the methods that run most often or whose failure is most expensive, annotate those, fix the inference complaints the compiler raises, and verify with the compile-and-inspect steps above. Keep scripts, builders, and DSLs dynamic where that flexibility is the point.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.