How to Convert a Single-Project Build into a Multi-Project Build in Gradle
When your Gradle project grows, you may want to split it into multiple subprojects to improve modularity, maintainability, and parallel execution.
This guide explains how to convert a single-project build into a multi-project build.
Why Use a Multi-Project Build?
A single-project build manages all source code and dependencies in one build.gradle(.kts)
file.
In contrast, a multi-project build consists of:
-
A root project that contains shared configurations and dependencies, as defined in
settings.gradle(.kts)
. -
Multiple subprojects, each with its own
build.gradle(.kts)
, dependencies, and tasks.
Step 1: Project Structure
Let’s start with a typical single-project setup, created by gradle init
:
root
├── settings.gradle(.kts) (1)
└── app (2)
├── build.gradle(.kts)
└── src/
1 | Defines the root project settings |
2 | The single application module |
Alternatively, your single project may look like this:
app (root) (1)
├── build.gradle(.kts)
├── settings.gradle(.kts)
└── src/
1 | The project root, also acting as the application module |
If your project looks like this, nest the app folder inside a new root folder before your get started.
|
After converting it into a multi-project build, the structure will look like this:
root
├── settings.gradle(.kts) (1)
├── app (2)
│ ├── build.gradle(.kts)
│ └── src/
└── library (3)
├── build.gradle(.kts)
└── src/
1 | Defines the subprojects |
2 | The original application module, now a subproject |
3 | A new library module |
Step 2: Create One or More New Subprojects
Now you can create your additional projects as directories in the root
.
They will all become subprojects of the root.
To continue our example, we create a new subproject called library
:
cd root
mkdir library
Ensure that the original project (app
) remains at the same level as the new library
subproject:
root
├── app (1)
│ └── some files
└── library (2)
1 | The original application module |
2 | The newly created subproject |
Move source files into their appropriate folders.
In our example, the reusable code is moved to the library
subproject:
root
├── app (1)
│ └── src/
└── library (2)
└── src/
1 | The application module, containing its own source code |
2 | The new library module, storing reusable code |
Step 3: Update settings.gradle(.kts)
Ensure that settings.gradle(.kts)
is located in the root directory:
root
├── settings.gradle(.kts) (1)
├── app (2)
│ └── src/
└── library (3)
└── src/
1 | The single settings file for the entire build |
2 | The application module |
3 | The new library module |
Modify settings.gradle(.kts)
to include the new subprojects.
In our example, we add library
to our include
:
rootProject.name = "root"
include("app", "library")
rootProject.name = "root"
include("app", "library")
There should be no other settings files.
Step 4: Create build.gradle(.kts)
Files for Each Subproject
Each subproject must have its own build.gradle(.kts)
file:
root
├── settings.gradle(.kts) (1)
├── app (2)
│ ├── build.gradle(.kts) (3)
│ └── src/
└── library (4)
├── build.gradle(.kts) (5)
└── src/
1 | Defines the subprojects |
2 | The application subproject |
3 | Defines the app build |
4 | The library subproject |
5 | Defines the library build |
The app
build file should look very similar to the original build file.
It holds all the dependencies and configurations needed to build the app
subproject.
Now, it also has a dependency on the library
subproject:
plugins {
application
id("org.jetbrains.kotlin.plugin.serialization") // Now the plugin is applied
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.3.1-jre")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation(project(":library")) // Uses the library module
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = "org.example.App"
}
plugins {
id('application')
id("org.jetbrains.kotlin.plugin.serialization") // Now the plugin is applied
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.guava:guava:33.3.1-jre")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation(project(":library")) // Uses the library module
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = "org.example.App"
}
The library
build file should be new.
It holds all the dependencies and configurations needed to build the library
subproject:
plugins {
id("java-library")
id("org.jetbrains.kotlin.plugin.serialization") // Now the plugin is applied
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.17.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
plugins {
id("java-library")
id("org.jetbrains.kotlin.plugin.serialization") // Now the plugin is applied
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.apache.commons:commons-lang3:3.17.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}
Step 5: Modify or Delete the Root build.gradle(.kts)
A build file in the root project is optional:
root
├── settings.gradle(.kts) (1)
├── build.gradle(.kts) (2)
├── app (3)
│ ├── build.gradle(.kts)
│ └── src/
└── library (4)
├── build.gradle(.kts)
└── src/
1 | Defines the subprojects |
2 | Optional (for shared configurations) |
3 | The application module |
4 | The new library module |
If you need to configure shared repositories and settings, you can use the root build.gradle(.kts)
:
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.21" apply false
}
subprojects {
repositories {
mavenCentral()
}
}
plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21' apply false
}
subprojects {
repositories {
mavenCentral()
}
}
You can take a look back at how the common 3rd party plugin called org.jetbrains.kotlin.plugin.serialization
is applied in each build file.
However, shared logic should be handled using convention plugins rather than the root build file.
Summary
This process can be repeated indefinitely to add more subprojects:
root
├── settings.gradle(.kts) (1)
├── app (2)
│ ├── build.gradle(.kts)
│ └── src/
├── library-a (3)
│ ├── build.gradle(.kts)
│ └── src/
├── library-b (4)
│ ├── build.gradle(.kts)
│ └── src/
└── library-c (5)
├── build.gradle(.kts)
└── src/
1 | Defines the subprojects |
2 | The application module |
3 | A shared library module |
4 | Another library module |
5 | Yet Another library module |
By following these steps, you can modularize your project, improve maintainability, and enable parallel execution.
If you wanted one of your libraries to be its own build, you should consider using Composite Builds (i.e., included builds) instead.