Spigradle supports Gradle multi-module projects, allowing you to organize your plugin code into multiple modules with shared dependencies and platform-specific implementations.
- Why Multi-Module?
- Requirements
- Project Structure
- Setup Guide
- Common Patterns
- Complete Examples
- Dependency Management
- Building Multi-Module Projects
- Best Practices
- Troubleshooting
- See also
Multi-module projects offer several benefits for Minecraft plugin development:
- Code Reuse: Share common code across multiple platform implementations (Spigot, BungeeCord, NukkitX)
- Separation of Concerns: Isolate platform-specific code from business logic
- API/Implementation Split: Expose a public API while keeping implementation details private
- Better Organization: Manage large codebases with clear module boundaries
- Independent Versioning: Version modules independently if needed
- Cleaner Dependencies: Each module declares only the dependencies it needs
Common use cases:
- Cross-platform plugins (Spigot + BungeeCord + Velocity)
- Plugin suite with shared utilities
- Plugin with public API for developers
- Large plugins split into feature modules
- Gradle 9.0+ (the latest version is recommended)
- Spigradle 4.0.2 or higher
- Basic understanding of Gradle multi-module projects
To update your gradle wrapper:
gradlew wrapper --gradle-version 9.2.1 --distribution-type allA typical multi-module Spigradle project structure:
my-plugin/
├── settings.gradle # Declares all modules
├── build.gradle # Root build configuration
├── gradle.properties # Shared properties (including version)
├── gradle/
├── └── libs.versions.toml # your unique dependencies
├── common/ # Shared code module
│ ├── build.gradle
│ └── src/main/java/...
├── spigot/ # Spigot implementation
│ ├── build.gradle
│ └── src/main/java/...
├── bungee/ # BungeeCord implementation
│ ├── build.gradle
│ └── src/main/java/...
└── api/ # Public API (optional)
├── build.gradle
└── src/main/java/...
Define all modules in your project.
Gradle supports importing multiple version catalogs from
settings.gradle(.kts)viadependencyResolutionManagement { versionCatalogs { ... } }. See Gradle’s “Version Catalogs” docs for details. citeturn0search0
Groovy DSL:
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
// For Spigot modules
create('spigots') {
from("io.typst:spigot-catalog:1.0.0")
}
// For BungeeCord modules
create('bungees') {
from("io.typst:bungee-catalog:1.0.0")
}
// For common plugins/dependencies (idea-ext, lombok, etc.)
// NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
create('commons') {
from("io.typst:common-catalog:1.1.0")
}
}
}
rootProject.name = 'my-plugin'
include('common', 'spigot', 'bungee', 'api') // api is optional
// OPTIONAL: rename child projects.
// Purpose: archiveBaseName / Maven artifactId derived from project.name
// rootProject.children.each { p ->
// p.name = "my-plugin-${p.name}"
// }Kotlin DSL (settings.gradle.kts):
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
// For Spigot modules
create("papers") {
from("io.typst:spigot-catalog:1.0.0")
}
// For BungeeCord modules
create("bungees") {
from("io.typst:bungee-catalog:1.0.0")
}
// For common plugins/dependencies (idea-ext, lombok, etc.)
// NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
create("commons") {
from("io.typst:common-catalog:1.1.0")
}
}
}
rootProject.name = "my-plugin"
include("common", "spigot", "bungee", "api") // api is optional
// OPTIONAL: rename child projects.
// Purpose: archiveBaseName / Maven artifactId derived from project.name
// rootProject.children.forEach { p ->
// p.name = "my-plugin-${p.name}"
// }Important:
ideaExtis only in thecommonscatalog (not in platform catalogs).
Configure shared settings for all subprojects.
Important: Version is managed centrally in
gradle.properties. Do not setversion = "1.0.0"(or similar) in build scripts.
Groovy DSL
plugins {
id 'base'
}
// You can configure shared settings here (for example, repositories and dependencies),
// but I recommend configuring them explicitly in each submodule.
allprojects {
group = 'com.example.myplugin'
// version is managed by gradle.properties (do not set it here)
}Kotlin DSL (build.gradle.kts)
plugins {
id("base")
}
// You can configure shared settings here (for example, repositories and dependencies),
// but I recommend configuring them explicitly in each submodule.
allprojects {
group = "com.example.myplugin"
// version is managed by gradle.properties (do not set it here)
}Example gradle.properties (root):
# Project version (centralized)
version=1.0.0
# External catalogs (centralized)
catalog.spigot.version=1.0.0
catalog.bungee.version=1.0.0
catalog.common.version=1.0.0Example gradle/libs.version.toml (root):
[versions]
gson = "2.10.1"
[libraries]
gson = {group = "com.google.code.gson", name = "gson", version.ref = "gson"}The common module has no platform-specific dependencies.
Groovy DSL
plugins {
id 'java'
// or: id 'org.jetbrains.kotlin.jvm' version '2.2.21'
}
repositories {
mavenCentral()
}
// No Spigradle plugin needed for common module
dependencies {
// Add common dependencies here
compileOnly 'org.jetbrains:annotations:24.0.1'
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}Kotlin DSL (common/build.gradle.kts)
plugins {
id("java")
// or: id("org.jetbrains.kotlin.jvm") version "2.2.21"
}
repositories {
mavenCentral()
}
// No Spigradle plugin needed for common module
dependencies {
// Add common dependencies here
compileOnly("org.jetbrains:annotations:24.0.1")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}Gradle’s Java toolchain configuration is done via the
java { toolchain { languageVersion = JavaLanguageVersion.of(...) } }block. citeturn0search1
Important: Platform plugins do not apply
javaautomatically. Addjavaororg.jetbrains.kotlin.jvm(version2.2.21) explicitly.
Groovy DSL
plugins {
id 'java'
// Spigradle plugin (via version catalog)
alias(papers.plugins.spigot)
// Optional (commons catalog only)
// alias(commons.plugins.ideaExt)
// alias(commons.plugins.lombok)
}
repositories {
mavenCentral()
// Repository shortcuts
spigotRepos {
papermc()
}
}
dependencies {
// Depend on common module
implementation project(':common')
// Paper API (from catalog)
compileOnly papers.paper
}
spigot {
apiVersion = '1.21'
depend = ['Vault']
commands {
register('mycommand') {
description = 'My command'
}
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
// include the common in jar
tasks.jar {
dependsOn(':common:jar')
from project(':common').sourceSets.main.output
}Kotlin DSL (spigot/build.gradle.kts)
plugins {
id("java")
// Spigradle plugin (via version catalog)
alias(papers.plugins.spigot)
// Optional (commons catalog only)
// alias(commons.plugins.ideaExt)
// alias(commons.plugins.lombok)
}
repositories {
mavenCentral()
// Repository shortcuts
spigotRepos {
papermc()
}
}
dependencies {
// Depend on common module
implementation(project(":common"))
// Paper API (from catalog)
compileOnly(papers.paper.api)
}
spigot {
apiVersion = "1.21"
depends = listOf("Vault")
commands {
register("mycommand") {
description = "My command"
}
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// include the common in jar
tasks.jar {
dependsOn(":common:jar")
from(project(":common").sourceSets.main.get().output)
}Important: Platform plugins do not apply
javaautomatically. Addjavaororg.jetbrains.kotlin.jvm(version2.2.21) explicitly.
Groovy DSL
plugins {
id 'java'
// Spigradle plugin (via version catalog)
alias(bungees.plugins.bungee)
// Optional (commons catalog only)
// alias(commons.plugins.lombok)
}
repositories {
mavenCentral()
// Repository shortcuts
bungeeRepos {
sonatype()
minecraftLibraries()
}
}
dependencies {
// Depend on common module
implementation project(':common')
// BungeeCord API (from catalog)
compileOnly bungees.bungeecord
}
bungee {
author = 'YourName'
depends = ['SomePlugin']
}
// Shadow common module into final JAR
tasks.jar {
dependsOn(':common:jar')
from project(':common').sourceSets.main.output
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}Kotlin DSL (bungee/build.gradle.kts)
plugins {
id("java")
// Spigradle plugin (via version catalog)
alias(bungees.plugins.bungee)
// Optional (commons catalog only)
// alias(commons.plugins.lombok)
}
repositories {
mavenCentral()
// Repository shortcuts
bungeeRepos {
sonatype()
minecraftLibraries()
}
}
dependencies {
// Depend on common module
implementation(project(":common"))
// BungeeCord API (from catalog)
compileOnly(bungees.bungeecord)
}
bungee {
author = "YourName"
depends = listOf("SomePlugin")
}
// Shadow common module into final JAR
tasks.jar {
dependsOn(":common:jar")
from(project(":common").sourceSets.main.get().output)
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}The common module typically contains:
- Business logic independent of platform
- Data models and DTOs
- Utility classes
- Configuration handling
- Database access layer
- API interfaces
Key characteristics:
- No Spigradle plugin applied
- No platform-specific dependencies (Spigot, BungeeCord APIs)
- Can use libraries like Gson, databases, etc.
Example structure:
common/
└── src/main/java/com/example/myplugin/
├── config/
│ └── PluginConfig.java
├── model/
│ └── PlayerData.java
├── service/
│ └── DataService.java
└── util/
└── MessageFormatter.java
Each platform module contains:
- Platform-specific entry point (extends JavaPlugin/Plugin/PluginBase)
- Platform API integrations
- Event listeners
- Command implementations
- Platform-specific utilities
Example Spigot module:
spigot/
└── src/main/java/com/example/myplugin/spigot/
├── MySpigotPlugin.java # extends JavaPlugin
├── listeners/
│ └── PlayerJoinListener.java
└── commands/
└── MyCommand.java
Example BungeeCord module:
bungee/
└── src/main/java/com/example/myplugin/bungee/
├── MyBungeePlugin.java # extends Plugin
└── listeners/
└── ServerConnectListener.java
For plugins that expose a public API for other developers:
Groovy DSL (api/build.gradle):
plugins {
id 'java'
// Optionally apply platform plugin via catalog if your API needs it.
// alias(papers.plugins.spigot)
}
repositories {
mavenCentral()
spigotRepos { papermc() }
}
dependencies {
// API typically doesn't depend on implementation
compileOnly papers.paper // Only if needed
}
// tasks.detectSpigotEntrypoints.enabled = false
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}Kotlin DSL (api/build.gradle.kts):
plugins {
id("java")
// Optionally apply platform plugin via catalog if your API needs it.
// alias(papers.plugins.base)
}
repositories {
mavenCentral()
spigotRepos { papermc() }
}
dependencies {
// API typically doesn't depend on implementation
compileOnly(papers.paper.api) // Only if needed
}
// tasks.detectSpigotEntrypoints.enabled = false
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}Other modules depend on the API:
Groovy DSL:
// spigot/build.gradle
dependencies {
api project(':api') // Expose API to consumers
implementation project(':common') // Hide implementation
}Kotlin DSL:
// spigot/build.gradle.kts
dependencies {
api(project(":api")) // Expose API to consumers
implementation(project(":common")) // Hide implementation
}A complete example of a plugin that works on both Spigot and BungeeCord:
Project structure:
my-plugin/
├── settings.gradle
├── build.gradle
├── common/
│ ├── build.gradle
│ └── src/main/java/com/example/
│ ├── MyPluginConfig.java
│ └── MessageService.java
├── spigot/
│ ├── build.gradle
│ └── src/main/java/com/example/spigot/
│ ├── MySpigotPlugin.java
│ └── listeners/PlayerListener.java
└── bungee/
├── build.gradle
└── src/main/java/com/example/bungee/
├── MyBungeePlugin.java
└── listeners/ProxyListener.java
Common module code:
// common/src/main/java/com/example/MessageService.java
package com.example;
public class MessageService {
public String formatMessage(String player, String message) {
return "[MyPlugin] " + player + ": " + message;
}
}Spigot module code:
// spigot/src/main/java/com/example/spigot/MySpigotPlugin.java
package com.example.spigot;
import com.example.MessageService;
import org.bukkit.plugin.java.JavaPlugin;
public class MySpigotPlugin extends JavaPlugin {
private MessageService messageService;
@Override
public void onEnable() {
messageService = new MessageService();
getLogger().info("Spigot plugin enabled!");
}
}BungeeCord module code:
// bungee/src/main/java/com/example/bungee/MyBungeePlugin.java
package com.example.bungee;
import com.example.MessageService;
import net.md_5.bungee.api.plugin.Plugin;
public class MyBungeePlugin extends Plugin {
private MessageService messageService;
@Override
public void onEnable() {
messageService = new MessageService();
getLogger().info("BungeeCord plugin enabled!");
}
}For complex multi-module projects, use build-logic composite build with convention plugins to share build logic. This is the modern approach recommended by Gradle over buildSrc.
Project structure:
my-plugin/
├── build-logic/
│ ├── settings.gradle.kts
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ ├── common-conventions.gradle.kts
│ ├── spigot-conventions.gradle.kts
│ └── bungee-conventions.gradle.kts
├── settings.gradle.kts
├── build.gradle.kts
└── ...modules...
Step 1: Configure build-logic/settings.gradle.kts:
rootProject.name = "build-logic"
versionCatalogs {
// For Spigot modules
create('spigots') {
from("io.typst:spigot-catalog:1.0.0")
}
// For BungeeCord modules
create('bungees') {
from("io.typst:bungee-catalog:1.0.0")
}
// For common plugins/dependencies (idea-ext, lombok, etc.)
// NOTE: commons catalog = idea-ext, lombok (bStats is NOT here)
create('commons') {
from("io.typst:common-catalog:1.1.0")
}
}Step 2: Configure build-logic/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
// Add Spigradle plugin(s) to convention plugin classpath if needed
implementation(papers.spigradleSpigot)
implementation(papers.spigradleBungee)
}Step 3: Create convention plugins:
build-logic/src/main/kotlin/common-conventions.gradle.kts:
plugins {
id("java")
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}build-logic/src/main/kotlin/spigot-conventions.gradle.kts:
plugins {
id("common-conventions")
alias(papers.plugins.spigot)
}
repositories {
spigotRepos { papermc() }
}
dependencies {
compileOnly(papers.paper.api)
}
spigot {
apiVersion = "1.21"
}build-logic/src/main/kotlin/bungee-conventions.gradle.kts:
plugins {
id("common-conventions")
alias(bungees.plugins.bungee)
}
repositories {
bungeeRepos {
sonatype()
minecraftLibraries()
}
}
dependencies {
compileOnly(bungees.bungeecord)
}Step 4: Include build-logic in main settings.gradle.kts:
pluginManagement {
includeBuild("build-logic")
}
rootProject.name = "my-plugin"
include("common", "spigot", "bungee", "api")Step 5: Apply conventions in module build files:
common/build.gradle.kts:
plugins {
id("common-conventions")
}
dependencies {
compileOnly("org.jetbrains:annotations:24.0.1")
}spigot/build.gradle.kts:
plugins {
id("spigot-conventions")
}
dependencies {
implementation(project(":common"))
}
spigot {
depends = listOf("Vault")
commands {
register("mycommand") {
description = "My command"
}
}
}
tasks.jar {
dependsOn(":common:jar")
from(project(":common").sourceSets.main.get().output)
}bungee/build.gradle.kts:
plugins {
id("bungee-conventions")
}
dependencies {
implementation(project(":common"))
}
bungee {
author = "YourName"
depends = listOf("SomePlugin")
}
tasks.jar {
dependsOn(":common:jar")
from(project(":common").sourceSets.main.get().output)
}Benefits of build-logic over buildSrc:
- Faster builds: Changes to convention plugins don't invalidate the entire build cache
- Better IDE performance: Gradle doesn't treat it as a special directory
- Explicit dependency management: You control when convention plugins are rebuilt
- Can be published as a separate artifact if needed
Using implementation:
Groovy DSL:
// Hides the dependency from consumers
implementation project(':common')Kotlin DSL:
// Hides the dependency from consumers
implementation(project(":common"))Using api:
Groovy DSL:
// Exposes the dependency to consumers (requires java-library plugin)
api project(':api')Kotlin DSL:
// Exposes the dependency to consumers (requires java-library plugin)
api(project(":api"))Each platform module should declare its own dependencies:
Groovy DSL:
// spigot/build.gradle
dependencies {
implementation project(':common')
compileOnly papers.paper
implementation 'com.google.code.gson:gson:2.10.1'
}
// bungee/build.gradle
dependencies {
implementation project(':common')
compileOnly bungees.bungeecord
implementation 'com.google.code.gson:gson:2.10.1'
}Kotlin DSL:
// spigot/build.gradle.kts
dependencies {
implementation(project(":common"))
compileOnly(papers.paper.api)
implementation("com.google.code.gson:gson:2.10.1")
}
// bungee/build.gradle.kts
dependencies {
implementation(project(":common"))
compileOnly(bungees.bungeecord)
implementation("com.google.code.gson:gson:2.10.1")
}Groovy DSL:
// In build.gradle
dependencies {
implementation libs.gson
}Kotlin DSL:
// In build.gradle.kts
dependencies {
implementation(libs.gson)
}For Bukkit plugin, use the libraries option in plugin.yml instead of shadowJar:
Groovy DSL:
// spigot/build.gradle
plugins {
id 'java'
alias(papers.plugins.spigot)
}
// ...
dependencies {
// compileOnlySpigot will be exported to plugin.yml libraries and compileOnly
compileOnlySpigot libs.gson
}
// ...Kotlin DSL:
// spigot/build.gradle.kts
plugins {
id("java")
alias(papers.plugins.spigot)
}
// ...
dependencies {
// compileOnlySpigot will be exported to plugin.yml libraries and compileOnly
compileOnlySpigot(libs.gson)
}
// ...When using Shadow plugin to bundle dependencies:
Groovy DSL:
// bungee/build.gradle
plugins {
id 'java'
id("com.gradleup.shadow") version "9.2.2"
alias(bungees.plugins.bungee)
}
shadowJar {
// Relocate to avoid conflicts
relocate 'com.google.gson', 'com.example.myplugin.lib.gson'
}
// Register the shadowJar task
tasks {
assemble.dependsOn(shadowJar)
}Kotlin DSL:
// bungee/build.gradle.kts
plugins {
id("java")
id("com.gradleup.shadow") version "9.2.2"
alias(bungees.plugins.bungee)
}
tasks.shadowJar {
// Relocate to avoid conflicts
relocate("com.google.gson", "com.example.myplugin.lib.gson")
}
// Register the shadowJar task
tasks {
assemble {
dependsOn(shadowJar)
}
}# Build all modules
./gradlew assemble
# Build specific module
./gradlew :spigot:assemble
# Clean and build
./gradlew clean assemble
# NOTE: build = build with test
# NOTE: assemble = build without testGradle automatically handles build order based on dependencies:
commonmodule builds firstspigotandbungeebuild after (since they depend oncommon)
Built JARs are located in each module's build/libs/ directory.
If you enabled the optional rootProject.children renaming in settings.gradle(.kts), the jar base names follow the renamed project names (useful for artifactId/archive naming). Otherwise, they follow the default module names.
Example (with renaming enabled):
my-plugin/
├── common/build/libs/my-plugin-common-1.0.0.jar
├── spigot/build/libs/my-plugin-spigot-1.0.0.jar
└── bungee/build/libs/my-plugin-bungee-1.0.0.jar
For distribution, you typically only need platform-specific JARs:
my-plugin-spigot-1.0.0.jar(includes common code)my-plugin-bungee-1.0.0-all.jar(includes common code through ShadowJar)
The my-plugin-common-1.0.0.jar is an intermediate artifact not needed for distribution.
Good:
// common module
public interface MessageSender {
void sendMessage(UUID playerId, String message);
}Bad:
// common module - DON'T DO THIS
import org.bukkit.entity.Player; // Platform-specific!
public class MessageService {
public void send(Player player, String msg) { }
}Avoid static dependencies between modules:
Good:
// Spigot module
public class MySpigotPlugin extends JavaPlugin {
private final MessageService messageService;
public MySpigotPlugin() {
this.messageService = new MessageService(new SpigotMessageSender());
}
}Bad:
// Using static references
MessageService.getInstance().send(...);Keep all modules at the same version by managing it centrally in gradle.properties:
version=1.0.0For shared resources (like messages, configs), include them in the common module and copy to platform modules:
Groovy DSL:
// spigot/build.gradle
processResources {
from(project(':common').sourceSets.main.resources)
}Kotlin DSL:
// spigot/build.gradle.kts
tasks.processResources {
from(project(":common").sourceSets.main.get().resources)
}Spigradle's main class detection works independently in each platform module:
spigotmodule: Detects class extendingJavaPluginbungeemodule: Detects class extendingPlugincommonmodule: No main class needed
Each module can have its own test dependencies:
Groovy DSL:
// spigot/build.gradle
dependencies {
testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation project(':common')
}
// common/build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
}Kotlin DSL:
// spigot/build.gradle.kts
dependencies {
testImplementation("org.mockito:mockito-core:5.8.0")
testImplementation(project(":common"))
}
// common/build.gradle.kts
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
}Cause: Module A depends on Module B, and Module B depends on Module A
Solution: Restructure your modules to have a clear dependency hierarchy:
api → common → spigot/bungee
Cause: Main class might be in common module instead of platform module
Solution: Ensure your JavaPlugin/Plugin class is in the platform-specific module:
✓ spigot/src/.../MySpigotPlugin.java extends JavaPlugin
✗ common/src/.../MyPlugin.java extends JavaPlugin
Cause: Common module classes not included in final JAR
Solution: Add common module output to JAR:
Groovy DSL:
tasks.jar {
from project(':common').sourceSets.main.output
}Kotlin DSL:
tasks.jar {
from(project(":common").sourceSets.main.get().output)
}Or use Shadow plugin to bundle:
Groovy DSL:
shadowJar {
from project(':common').sourceSets.main.output
}Kotlin DSL:
tasks.shadowJar {
from(project(":common").sourceSets.main.get().output)
}Cause: Multiple modules contribute the same resource files
Solution: Use duplicatesStrategy:
Groovy DSL:
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from project(':common').sourceSets.main.output
}Kotlin DSL:
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(project(":common").sourceSets.main.get().output)
}- The Spigot plugin
- The Bungeecord plugin
- The Nukkit plugin
- README.md
- Gradle Multi-Project Builds
- Gradle Multi-Project Builds (rootProject.name)
- Gradle Version Catalogs
- Gradle Toolchains for JVM projects
- Gradle Java Library Plugin
- Shadow Plugin