Getting started
This page walks through the smallest useful program and explains exactly what the container does. For installation coordinates, see the main README. The library targets Java 21 and lives under be.theking90000.scope.
A first scope
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 service = root.get(Service.class);Every scope is parameterized by a context object (RootScope here). The context identifies the scope and is itself injectable.
What get(Service.class) does
When root.get(Service.class) is called:
- The scope looks for a provider of
Service. - It finds none, so it creates
Serviceautomatically. - The injector inspects the single public constructor of
Service. - It resolves
Config(found via theseedabove). - It instantiates
Service(config). - The new instance is cached in the scope as a scope singleton — the next
get(Service.class)returns the same object.
That is the whole loop: look up, otherwise build by constructor injection, cache.
Read as the lexical mental model, the scope is a block and get fills in the one missing name:
┌─ root : RootScope ───────────────────────────────────────────────┐
│ │
│ RootScope = (context) ┐ auto-seeded │
│ Scope = root ┘ on creation │
│ │
│ Config = Config("prod") (seeded) │
│ │
│ Service = Service(config) ◄── built by get(), cached here │
│ └─ needs Config ─► Config above │
│ │
└──────────────────────────────────────────────────────────────────┘A type you never registered (like Service here) is built in the scope you call get on. That choice decides its lifetime — see where an auto-created bean lands.
What every scope already knows
A freshly constructed scope seeds two things automatically:
- its context object (here
RootScope); - itself, under
Scope.class.
So a class created in this scope can inject either of them:
record NeedsScope(RootScope root, Scope<?> scope) {}
NeedsScope n = root.get(NeedsScope.class);
// n.root() == the RootScope context
// n.scope() == the nearest Scope (see shadowing rules)Three ways to put something in a scope
| Method | Use it for |
|---|---|
seed(type, instance) | An object already created by your code (plugin, config, player, server handle). |
provide(type, factory) | A factory the container calls lazily the first time the value is needed. |
bind(type) | A type the container should construct later, without validating its dependency graph now. |
root.seed(Config.class, new Config("prod")); // existing instance
root.provide(Clock.class, () -> new Clock(now())); // lazy factory
root.bind(Service.class); // construct on first get()All three return the scope, so calls chain:
root.seed(Config.class, cfg)
.provide(Clock.class, () -> new Clock(now()))
.bind(Service.class);Next: Injection — the rules the container follows when it builds a class for you.