First-class scopes for the JVM: create, nest, shadow and dispose runtime scopes — with automatic dependency injection.
scope models scopes as first-class runtime objects. Scopes are built,
nested, overridden and disposed while the program runs — the way lexical blocks
behave in JavaScript or Rust, but reshaped at runtime instead of fixed at compile
time. A small dependency injection layer resolves dependencies along the scope
graph.
Manage lifetimes and visibility at runtime — plugin, player, game, action — as nestable scopes with automatic wiring.
It is built for systems that juggle many overlapping lifetimes and visibilities
at once. On a game server like Minecraft, the same value can live at the scale of
the plugin, a player, a running game, or a single action — each with its own
boundary of visibility, state and lifecycle. scope models each of those as a
scope, and resolves dependencies along the scope graph.
A Scope reads like a lexical block in JavaScript, Rust or Java. A value defined
in an outer block is visible to inner blocks, unless an inner block defines a
nearer value under the same key — in which case the nearer one shadows it.
// RootScope
{
x = value;
// PlayerScope
{
player = value;
classes = ...;
// An object created here sees:
// - player and classes locally;
// - x from the parent;
// - any local definition shadowing a parent definition.
}
// GameScope
{
game = value;
players = List<Scope<Player>>;
// Player1Scope
{ player = player1; }
// Player2Scope
{ player = player2; }
}
}
The difference with the compiler's version: these blocks are objects. They are opened, nested across multiple parents, shadowed and closed at runtime — not fixed at compile time. Default resolution uses the nearest definition, so a local value shadows a parent's value exactly like a local variable.
Most DI frameworks treat scoping as a feature bolted onto a container. Here the scope is the model, and dependency injection is just what falls out of it.
-
The scope is the feature, not a bonus. There is no
@Singleton, no@RequestScoped, no fixed zoo of predefined scopes. There is one concept — the scope — and everything is a singleton within its scope. Scopes are made to be created, nested, manipulated and closed on the fly, not declared once at startup. -
An API kept deliberately far from the domain. A
Scopeknows nothing about annotations, classloaders, "events" or "requests". It knows nested spaces of variable visibility, nothing more. Domain vocabulary — player, game, plugin — stays in application code, layered on top. -
Lightweight, dynamic DI. No heavy machinery: it resolves a constructor from what is present in the environment, walking up the scope graph, and instantiates in the current scope when nothing is found along the way. That is the whole wiring story.
-
close()is at the core, not an afterthought. Closing a scope releases all of its resources deterministically and on purpose, so domain logic runs before the GC ever sees the objects (@PreDestroy,AutoCloseable, disposers). Timing and order are explicit (LIFO) — release does not wait on the collector. -
Extension points designed to be shadowed.
OnCreatedHooks are not a frozen global mechanism: they follow the same shadowing rules as everything else, so a child scope can override or rewrite a hook locally — again with deterministic cleanup through disposers.
| Guice | Avaje | ActiveJ | tiko-di | scope | |
|---|---|---|---|---|---|
| Hierarchical scopes | ✅ | ✅ | ✅ | ✅ | |
| Multi-parent (DAG) | ❌ | ❌ | ❌ | ❌ | ✅ |
| Lexical shadowing (nearest definition wins) | ❌ forbidden | ❌ | ❌ | ❌ | ✅ |
| Runtime-manipulable scope graph (create/drop/override) | ❌ | ❌ | ❌ | ❌ | ✅ |
| Scope is the model (DI is a consequence) | ❌ | ❌ | ❌ | ❌ | ✅ |
| No "singleton" notion (everything is scope-singleton) | ❌ | ❌ | ❌ | ❌ | ✅ |
Deterministic close() (domain cleanup before GC) |
❌ | ✅ | ✅ | ✅ | |
| Domain-agnostic API (no annotations / classloaders) | ❌ | ❌ | ❌ | ❌ | ✅ |
| Resolution | runtime | compile | runtime | compile | runtime |
The differentiator is not any single cell — it is having all of them at once: a multi-parent, lexically-shadowed scope graph you reshape at runtime, scope-first.
Performance and memory benchmarks — the evidence behind the "lightweight,
dynamic DI" claim — live in
scope-benchmarks/: per-scope create/close cost,
memory footprint, leak-after-close() checks and scaling charts across several
machines.
import be.theking90000.scope.Scope;
record RootScope() {}
record Config(String value) {}
record Service(Config config) {}
Scope<RootScope> root = new Scope<>(new RootScope());
root.seed(Config.class, new Config("prod"));
// Service has no provider, so the container builds it:
// it inspects the single public constructor, resolves Config,
// instantiates Service(config) and caches it as a scope singleton.
Service service = root.get(Service.class);Scopes nest and close deterministically:
record Player(String name) {}
record Session(Player player, RootScope root) {}
Scope<Player> player = new Scope<>(new Player("Ada"));
player.ownedBy(root); // sees root; root owns and will close it
Session session = player.get(Session.class); // Player from here, RootScope from parent
player.close(); // runs @PreDestroy / AutoCloseable / disposers, LIFOThe library targets Java 21 and is published to GitHub Packages. GitHub
Packages requires authentication for reads, so a token is needed (a classic PAT
with the read:packages scope is enough).
Coordinates are
be.theking90000:scope:1.0.0, published under thetheking90000/scoperepository. The artifact is not published yet — build from source in the meantime.
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/theking90000/scope")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation("be.theking90000:scope:1.0.0")
}<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/theking90000/scope</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>be.theking90000</groupId>
<artifactId>scope</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>Credentials go in ~/.m2/settings.xml under a matching <server><id>github</id></server>.
./gradlew :scope:build
./gradlew :scope:testThe hosted docs are available at https://theking90000.github.io/scope/.
The full English guide lives in docs/ — start with the
mental model, then the
API reference, extension hooks,
multi-parent scopes and lifecycle.
Every public type ships with thorough JavaDoc: https://theking90000.github.io/scope/javadoc/.
The original in-depth reference, in French, is
scope/README.md.