Code Organisation and Patterns for Minecraft Mods
This is a Garden Post. It may be unfinished or changing over time.People write their mods all sorts of ways! This is an attempt at writing up how I like to do it.
Some Caveats #
- This is mostly thinking about Fabric on modern (as of 2025) Minecraft versions.
- A lot of this may be applicable to other modloaders and versions, but not all.
- This is just what works for me!
- Some people might hate this!
The tips #
Mojmap #
I'm a Yarn/QM Hater!
Parchment is good. Never forget to add Parchment.
Split Source Sets #

Nobody uses this and it blows my mind!
Split source sets can be enabled in your build.gradle with:
loom {
splitEnvironmentSourceSets()
mods {
"your_mod_id" {
sourceSet sourceSets.main
sourceSet sourceSets.client
}
}
}
You now have code in src/main/ and src/client/. The code in src/main/ cannot reference any client code! This completely eliminates all "whoops I accidentally tried to classload class_310 on the server" crashes.
but Will I wanna call
SoundManager#playin my Block Entity client ticker
Client Proxy #
WHAT IS THIS, 2015??
Yeah that's right babey we're going BACK!!!!
Ok so I'm not really advocating for a return of @SidedProxy and all. But if I ever need to use some client-only behaviour on the Common side, I do so with a ClientHandler. It works like this:
src/main/java/com/example/ClientHandler.java:
public interface ClientHandler {
void playMySpecialSound(BlockPos pos);
ClientHandler INSTANCE =
(FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT
? Stream.<ServiceLoader.Provider<ClientHandler>>empty() : ServiceLoader.load(ClientHandler.class).stream())
.findFirst()
.map(ServiceLoader.Provider::get)
.orElseGet(ClientHandler::getDummyClientHandler);
static ClientHandler getDummyClientHandler() {
return new ClientHandler() {
@Override
public void playMySpecialSound(BlockPos pos) {
}
};
}
}
src/client/java/com/example/client/ClientHandlerImpl.java:
public class ClientHandlerImpl implements ClientHandler {
@Override
public void playMySpecialSound(BlockPos pos) {
// whatever
}
}
src/client/resources/META-INF/services/com.example.ClientHandler:
com.example.client.ClientHandlerImpl
That's it! Just an interface with the methods you want to call, loaded with a ServiceLoader. Safe! This does require you to manually define everything you want to do that has to cross the server-client boundary in this way, but that's actually an advantage if you ask me: it forces you to think about exactly what you need, and minimises the crossings.
But I could just put client and common together and use
@Environment(EnvType.CLIENT)!
You could but it's sooo easy to mess up. Did you know that if you @Environment a field, although it gets stripped away,
its initialiser doesn't? So your
@Environment(EnvType.CLIENT) public int myValue = 100;
crashes on server just by existing :(
Using @Environment essentially causes you more opportunities to have these types of crashes.
Okay but I really can't use split source sets so I'm just going to put some methods in my common classes and be careful not to touch them on server
Pleeaase segregate the client only methods into an inner class - you really can't guarantee that the JVM won't suddenly decide to Check That Your Types Actually Exist otherwise.
Datagen all the things! #
It's 2025, use datagen. You don't want to be having to write those JSON files by hand.
.gitignore #
I've thought a lot about this. I used to say that you should gitignore your src/main/generated folder. My reasoning was that the things in there are essentially build artifacts. I have since changed my mind.
Commit your src/main/generated (but gitignore src/main/generated/.cache/) - here's why:
Gradle and the Minecraft modding ecosystem in general is so fickle that you cannot guarantee that a project that builds today will build tomorrow, or even today on a different computer.
There are random gradle bugs; people's mavens going down; people not specifying exact versions in their build.gradles and so when some random unpinned transitive dependency updates else updates everything breaks; different Java versions breaking things. I've had all of these happen to me at least once. It's not fun. And you don't want to have to lock people out of getting the resources because the code can't build.
This is especially helpful for people who want to make resource packs / datapacks for your mod, or people who want to contribute translations. Those people may not have a JDK installed or know how to run Gradle tasks. Commit your generated resources for them.
Still gitignore the src/main/generated/.cache folder though, there isn't any point in committing that and it just makes your diffs noisy.
Group By Feature #
Tidy Initialisers #
The initialiser for Elysium looks like:
@Override
public void onInitialize() {
RegistryEntryAttachment.init();
CheirosiphonItem.ServerboundAirblastPacket.init();
ElysiumMachines.init();
ElysiumArmour.init();
ElysiumSounds.init();
}
Honestly, there's one more line there than I'd prefer.
The client initialiser looks like:
@Override
public void onInitializeClient() {
ReaClient.clientInit();
CheirosiphonClientModule.init();
ElysiumMachinesClientModule.init();
ParticlesClientModule.init();
ElysiumFlameRendering.init();
GlowEffectManager.INSTANCE.init();
ElysiumArmourRenderer.setup();
}
I used to throw a load of stuff into my initialiser functions - dozens of event listeners!
The issue ends up being that your eyes start to glaze over and it becomes impossible to read properly.
What I like to do these days is split my initialisers up by feature - so that RegistryEntryAttachment.init() registers a resource reload listener, and a couple of event listeners. But it being sorted by-feature helps with:
- Updating - you can update one feature at a time, and it may be easier to disable features that don't compile yet, allowing you to run the rest of the mod. Much less demoralising than 'I must fix 1000 errors before I can even compile and see what works'.
- Reading - especially when you're coming back to the code after a while, it's way easier to get a feel for how everything comes together when it's organised like this.
Packages #
As I'm grouping by feature, I of course make the package structure in the same way - com.williambl.elysium.machine, rather than com.williambl.elysium.block.
I also mirror the package structure in the client (and in mixins - see below) - so for a feature myfeature, you'll have packages:
com.example.myfeaturecom.example.mixincom.example.client.myfeaturecom.example.client.mixin.myfeature
Registering #
I'm genuinely not sure what the best way to register things is.
- For small mods maybe you just want to throw everything into static fields on your main mod class?
- For bigger mods you have two options:
- Group them by-feature - e.g. in Elysium, the
ElysiumMachinesclass holds all the static fields for all the elysium machine blocks and items. - Group them by type - like Minecraft does.
Blocks,Items, etc.
- Group them by-feature - e.g. in Elysium, the
A note on class names #
If you're grouping registered things by type, please don't call them ModBlocks, ModItems, etc. when you're building an add-on mod and there are five different ModBlockses in your classpath it gets annoying.
(That said, it's been a while since I've done that. I think maybe if I were making an add-on mod these days I would just retrive Holders from the registry with ResourceKeys.)
Mixins #
You can mixin to the same class multiple times! You don't need to have one giant LivingEntityMixin that does 10000 things when you can have feature1.LivingEntityMixin, feature2.LivingEntityMixin etc. This brings all the advantages of group-by-feature development to mixins.
Components #
Uh, not Data Components, Text Components.
This isn't very relevent for many mods, but I like to have all my components, along with their keys, in a ModNameComponents class. e.g.:
public static final String MY_WIDGET_KEY = Util.makeDescriptionId("gui", id("my_widget"));
public static final Component MY_WIDGET = Component.translatable(MY_WIDGET_KEY);
You'll also see there that I like to use Util.makeDescriptionId - because it takes a Resource Location, you can be sure that your keys won't conflict with anyone else's :)
This is a departure from group-by-feature, but I feel like this works better? Rules are flexible, you know.