Understanding the Registration Chain
Now that you have registered items and blocks, this tutorial explains how the fluent chain works so you can use it confidently in more complex scenarios.
What Is a Registration Chain?
A registration chain is a sequence of method calls on a builder object that configures and then submits a game object to the registry. It follows the fluent API (or builder) pattern where each method returns the builder itself, allowing calls to be chained:
REGISTRYLIB
.item("example", Item::new) // 1. Create the builder
.lang("Example Item") // 2. Configure
.addDefaultTab() // 3. Configure more
.register(); // 4. Submit
Every chain follows the same lifecycle:
- Builder creation —a factory method on
RegistryCore(like.item(),.block(),.fluid()) creates a typed builder. - Configuration —zero or more chained calls set properties, lang entries, tags, loot, models, and other attributes.
.register()—the terminal operation that submits the builder's configuration to RegistryLib's internal registry and returns anEntry.
The Lifecycle in Detail
RegistryCore Builder Entry
— — —
├─ .item(...) ──────►│ —
— ├─ .lang(...) —
— ├─ .properties(...) —
— ├─ .addTag(...) —
— ├─ .register() ─────────►│
— — —
— —(builder is consumed) —(entry is returned)
After .register() is called:
- The builder's accumulated configuration is frozen and scheduled for registration during the appropriate NeoForge lifecycle events.
- A typed
Entryobject is returned immediately. TheEntryacts as a lazy supplier —calling.get()on it returns the actual registered object once the registry event has fired.
The Parent Type System
One of RegistryLib's most powerful features is nested builders. When you call .item(...) on a BlockBuilder, you enter an ItemBuilder whose parent is the block builder:
REGISTRYLIB
.block("example_block", Block::new) // BlockBuilder<Block, RegistryCore>
.initialProperties(() -> Blocks.STONE)
.item(item -> item // ItemBuilder<..., BlockBuilder>
.addDefaultTab()
.addTooltip(Component.literal("A tooltip"))
) // returns to BlockBuilder
.register(); // registers BOTH the block and its item
The generic parameter P (parent) tracks this relationship at the type level. When the inner builder finishes, control returns to the parent builder. A single .register() at the end submits the entire tree.
You do not need to call .register() inside the .item(...) callback. The outer .register() handles everything.
Entry Types
Every .register() call returns a typed Entry:
| Builder | Returns | .get() yields |
|---|---|---|
item(...) | ItemEntry<T> | T extends Item |
block(...) | BlockEntry<T> | T extends Block |
fluid(...) | FluidEntry<T> | T extends FlowingFluid |
The concrete entry types (ItemEntry, BlockEntry, FluidEntry) extend AbstractHolderEntry, which in turn extends RegistryEntry. AbstractHolderEntry implements Holder<R>, so entries can be passed directly to APIs that accept holders.
Entry objects are safe to store as static final fields. They are resolved lazily -- .get() returns the registered instance after the registry event fires. If the entry has not been bound yet (i.e. the registry event has not fired), .get() throws an IllegalStateException rather than returning null. Use .getOptional() when you need a safe check, or .isBound() to test whether the value is available:
// Store as a constant
public static final ItemEntry<Item> MY_ITEM = REGISTRYLIB
.item("my_item", Item::new)
.lang("My Item")
.register();
// Use later (after registration)
Item item = MY_ITEM.get(); // throws if not yet bound
ResourceKey<Item> key = MY_ITEM.getKey();
// Safe access patterns
Optional<Item> maybe = MY_ITEM.getOptional(); // empty before binding
boolean ready = MY_ITEM.isBound(); // false before binding
Common Pitfalls
Forgetting .register()
The most common mistake. Without .register(), the chain builds a configuration object that is never submitted:
// —Bug: no .register() —nothing is registered
REGISTRYLIB
.item("forgotten_item", Item::new)
.lang("Forgotten");
// —Correct
public static final ItemEntry<Item> MY_ITEM = REGISTRYLIB
.item("my_item", Item::new)
.lang("My Item")
.register();
Since there is no returned Entry, the compiler does not force you to capture the result. Always end with .register().
Registration Class Not Loaded
Java only initializes a class when it is first referenced. If your registration class is never referenced, its static final fields are never evaluated and nothing gets registered.
// In your mod constructor or initializer, force the class to load:
SimpleItemExample.init(); // a static method, or just reference any field
A common pattern is to add a no-op public static void init() {} method to each registration class and call it from your mod's constructor.
Calling .get() Too Early
Entry objects are lazy. Calling .get() before the registry event fires (e.g., during static initialization or in a constructor) throws an IllegalStateException. It never returns null.
// —Too early —registry event has not fired yet
public static final Item RAW = MY_ITEM.get(); // throws IllegalStateException
// —Use the Entry itself and call .get() when needed at runtime
public void someMethod() {
Item item = MY_ITEM.get(); // safe after registration
}
// —Use .isBound() or .getOptional() when you are unsure of timing
if (MY_ITEM.isBound()) {
Item item = MY_ITEM.get();
}
Summary
| Concept | Key Point |
|---|---|
| Chain structure | Create —Configure —.register() |
| Terminal operation | .register() submits and returns an Entry |
| Nested builders | .item(...) on a block builder creates a child; one .register() submits both |
| Entry objects | Lazy suppliers; store as static final, call .get() at runtime (throws if unbound; use .getOptional() or .isBound() for safe checks) |
| Class loading | Reference your registration class so its static fields are initialized |
What's Next
- Builder Pattern (Concepts) —deeper dive into the builder architecture and generics
- Group System —organize registrations into logical groups