Kotlin "Static" APIs
An afternoon experiment with Kotlin metaprogramming
March 23, 2026
I have been developing for the PvP Legacy Minecraft server for a while now. Starting with a revamped Discord bot and then moving onto plugins and other tools, Kotlin has always been my preference, despite (or perhaps because of) the dominance of Java in most of our codebases. In this post, I will explore a problem I have faced while designing APIs to be consumed by both languages, and how I ended up solving it by dipping my toes in some annotation processing. Hopefully, it will be useful to anyone else either developing such APIs or wanting to experiment with Kotlin metaprogramming.
# Plugins and APIs
One of the things I have developed for the server (some of which I hope to open-source) is a Paper plugin to improve the interplay between datapacks and plugins — DatapackUtils. It provides some useful vanilla-like extensions for datapacks and, most importantly to this post, APIs for plugins based on NMS.
For example, the Paper plugin API does not implement a way to interact with command storage, a very common feature used in datapacks; DatapackUtils covers this with its StorageUtils API.
But what is this API, really?
Well, in Minecraft server development, it is quite common to distribute interfaces which, instead of having their implementations compiled into the final über jars of each plugin, have their implementations provided by a separate plugin. The consumers simply pull in a thin compile-only library with the interface definitions, and declare a dependency on the providing plugin’s name. The server operators must then include the providing plugin when setting up. This is done because the implementation of such APIs typically must make use of plugin resources. They thus almost always end up getting “set” at runtime when the providing plugin loads, with consumers accessing the interfaces through a provider object or static method. In essence, it is similar to dynamic library loading, like we commonly see with C/C++ software, though with some heavy reliance on OOP; such is the fate of Java code.
For example, if I have an interface Foo, it is common to also expose in the library module a class like this:
class FooProvider {
private static Foo foo;
static void set(Foo foo) { this.foo = foo; }
static Foo get() { return foo; }
}
And then, always have the consumer access the API through FooProvider.get().someMethod(). Alternatively, you could remove the need for the extra static method call (which is also commonly called getInstance) by having FooProvider mirror all the interface methods with static ones that delegate to foo. The first solution is needlessly verbose and gets old fast, while the second solution requires duplicating the interface with static members, leaving room for silly mistakes.
If only there were a JVM language that played nicer with delegation…
# Kotlin in all this
I quite like Kotlin as a language, it has a nice set of features and addresses most of my pain points with Java. One of the coolest and most differentiating features it has is delegation, both for interfaces and properties. The latter allows interesting and often “type-safer” APIs, such as what you can find in Koin and Kord-Ex, but it’s not really relevant to this post — interface delegation is. In short, it allows you to declare that a class implements some interface by delegating all (non overridden) methods to an object implementing that interface, available in the constructor. Here’s an example:
interface Foo {
fun foo()
}
// the "by" keyword lets us specify which object we delegate to for Foo methods
class Bar(delegate: Foo) : Foo by delegate {
fun bar() = TODO()
// implicitly available:
// fun foo() = delegate.foo()
}
This feature, together with Kotlin’s object singletons, seems like a perfect fit for our API design problem — write an object that implements the API through delegation, so that you can set and swap out the underlying implementation when needed. Indeed, quite the perfect fit… except for two small snags:
You can’t really use mutable variables as interface delegates. The bytecode the compiler generates captures the value of the delegate when the delegating object is initialized, so changes to the var don’t actually reflect on the delegate; IntelliJ even warns you about it!
While using singletons is perfectly fine on the Kotlin side, the Java side would still have to do ObjectImplementingFoo.INSTANCE.foo(). This is tolerable if the significance of Java in your codebases is fairly reduced, but in my case it’s exactly the opposite.
:/
So, what gives now? Well, there is the @JvmStatic annotation which adds a static accessor to an object member, but there’s no built-in way to do it for delegated properties. Likewise, there is no built-in way to have delegate mutability. For a small number of small APIs, mirroring the methods by hand isn’t that big a deal. However, for larger ones, or if you have several different APIs, duplicating all the methods and documentation quickly gets tiring and error prone. As I ran into these two shortcomings, I thought to myself, in a defeatist tone, “well, short of doing some metaprogramming to generate an object with delegate mutability and @JvmStatic on all members, there’s nothing I can really do”.
I had always been a bit wary of metaprogramming in Java and Kotlin, to be frank. The need to write a whole separate package and to pull big annotation processing libraries screamed needless complexity, pushing me away. While they are still more complex than they really ought to be, I’ve come to learn that, at least with KSP, small modules are not the kind of Dark Souls final bosses I thought they were. .^.
I am also writing a followup post on how to implement a simple annotation processor with KSP and KotlinPoet. If you are curious about the specific implementation of this particular processor, you can find its source on Sourcehut or on Radicle.
# How does it work in practice?
For a given API interface:
/**
* API to work with foo and bar.
*/
interface IFooBar {
/** Makes a new foo. */
fun makeFoo(): Foo
/** Returns true if the given foo is also a bar. */
fun isBar(foo: Foo): Boolean
}
We just have to annotate it with @StaticApi, and we will get an automatically generated object somewhat like this:
/**
* API to work with foo and bar.
*/
object FooBar {
@ApiStatus.Internal lateinit var delegate: IFooBar
/** Makes a new foo. */
@JvmStatic fun makeFoo() = delegate.makeFoo()
/** Returns true if the given foo is also a bar. */
@JvmStatic fun isBar(foo: Foo) = delegate.isBar(foo)
}
Although the object itself does not implement the annotated interface (because static methods cannot override anything), the object fully mimics the interface (including generics and inherited methods), and even copies all KDoc comments over.
The delegate field should be set by the providing plugin at the earliest time possible. Because that is typically done across compilation modules, Kotlin’s internal modifier can’t be used, thus annotating it with @ApiStatus.Internal is the next best thing.
Note: The current implementation has a few limitations, because I haven’t yet put all the time needed to address them, this was a primarily an afternoon project after all! The main issues I have identified are the lack of support for properties and the generic uninitialization errors from lateinit variables.
To help with compatibility in potentially trickier edge cases, the annotation takes two optional parameters:
objectName: String — By default, the processor tries matching the annotated interface name against ^I[A-Z] and dropping the first character, but with this parameter you can force the generated object’s name to anything you want. If the name doesn’t match, this parameter must be specified, otherwise the processor will raise an error. Admittedly, this is a bit obtuse and may change in the future.
delegateName: String — By default, the backing delegate field will be named delegate, but since it is technically public (only marked as internal API with the annotation), there may be a need to tweak its name; that is possible with this parameter.
volatileDelegate: Boolean — In cases where setting and accessing the delegate may be very contended, setting this parameter to true will result in the delegate field being @Volatile.
I find this to be a rather nice solution to the problem, and I hope others might too. Sure, it might be an overengineered solution, but it certainly was a fun afternoon of learning :)
For now, you can use it by cloning the repository and publishing the project to your local Maven repo (publishToMavenLocal task). I will set up publishing to common package repositories soon! Check back later, I’ll update the post once that’s done.