Contents

gradle init for Spring Boot

Production-ready Gradle plugin inspired by DPE University courses.

Why yet another Gradle plugin

Recently, I wanted to deepen my understanding of Build Scans, Develocity, and their practical applications, so I started going through the courses on our DPE University platform. In my daily work, I interact with Develocity from the operational and production side — maintaining open-source instances among other things — so I wanted to better understand the product, especially from the end-user perspective, to support my daily engineering work.

To systematize what I was learning, I decided to build a project that aligns with the best practices discussed in these courses. At the same time, I was working on a Spring Boot microservice generated via Spring Initializr and noticed there was no Gradle plugin that would automate this process in a way that is build cache-friendly and CI/CD-friendly.

That’s how the gradle-springinitializr-plugin was born—a production-ready tool that simplifies bootstrapping Spring Boot projects directly from Gradle. This post covers how it was built, how it works, and how you can use it to speed up starting new projects within your team.

Quick start

The plugin is open-source and available on the Gradle - Plugins. You can install and start using it right away.

Create the build.gradle file and add the following:

1
2
3
plugins {
    id 'io.oczadly.springinitializr' version '1.0.0'
}

Create the settings.gradle file and add this configuration:

1
rootProject.name = 'examples-simple-groovy'

Or add it to your existing project.

This way, you can generate new Spring Boot projects with Gradle while reading this post, accelerating your onboarding and daily work.

👉 Gradle - Plugins

👉 GitHub

I treat this plugin as a reference: how to build a production-ready plugin in Groovy consistent with Gradle best practices. If you build plugins yourself, you can use it as a point of reference.


Who is this plugin for

Platform Engineers

gradle-springinitializr-plugin can become a part of your platform to accelerate microservice scaffolding across your organization. With it, you can:

✅ Quickly generate Spring Boot microservices (Groovy/Java/Kotlin, Gradle/Maven) from CLI, pipelines, or automated GitOps processes.

✅ Always generate projects with the latest Spring Boot, Java, and dependency versions, eliminating the need to manually maintain templates.

✅ Provide a build cache-aware, CI-friendly workflow from day one of your projects.

✅ Standardize project structure, metadata (groupId, artifactId, packageName, description), and the microservice creation process.

If you are building an engineering platform for JVM-based developer teams, this plugin can become a lightweight, configurable, and automatable pipeline element for “just-in-time” scaffolding of Spring Boot projects.

Software Engineers

gradle-springinitializr-plugin can become your go-to tool for quickly starting Spring Boot projects without leaving the Gradle ecosystem. Instead of clicking through the Spring Initializr web UI or writing curl commands, you can generate a fully customizable project with a single Gradle command. It saves time, ensures consistent configurations, and integrates immediately into your build cache, CI/CD, and team conventions.

The Curious and Mindful

gradle-springinitializr-plugin lets you see how Spring Boot project generation from Gradle works in under a minute, without clicking around https://start.spring.io or writing curl commands. Instead of manually downloading ZIP files, you can instantly start a project locally and explore Spring Boot in a repeatable, clean, and user-friendly way.


How it works

After installing the gradle-springinitializr-plugin, you can generate Spring Boot projects directly from Gradle without leaving your terminal.

The plugin provides a single task initSpringBootProject, which downloads a project from Spring Initializr and extracts it. Instead of clicking in the web interface, simply run:

1
gradle initSpringBootProject

You will get:

1
2
3
4
5
6
> Task :initSpringBootProject
Downloading Spring Boot starter project...
Project downloaded to: /opt/my-projects/build/generated-project/starter.zip
Project extracted to: /opt/my-projects/build/generated-project/demo

BUILD SUCCESSFUL in 1s

By default the project will be downloaded and extracted into build/generated-project/demo, ready to open and run.

The plugin also allows you to set any parameter available in Spring Initializr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
gradle initSpringBootProject \
    -PprojectType="gradle-project-kotlin" \
    -Planguage="kotlin" \
    -PgroupId="com.mycompany" \
    -PartifactId="my-spring-app" \
    -PprojectName="My Spring App" \
    -PprojectDescription="My Spring Boot application generated via gradle-springinitializr-plugin" \
    -PpackageName="com.mycompany.myspringapp" \
    -Ppackaging="war" \
    -PjavaVersion="21" \
    -Pdependencies="web,actuator"
    -PoutputDir="/opt/my-projects/my-spring-boot-app"

This way you generate clean, consistent projects ready for your CI/CD workflow and aligned with team conventions – without manual steps.

The plugin also supports an experimental interactive mode inspired by gradle init:

https://raw.githubusercontent.com/paweloczadly/gradle-springinitializr-plugin/refs/tags/v1.0.0/docs/images/demo.gif
gradle init for Spring Boot

This allows you to enter all project parameters without remembering command line flags – the plugin guides you step by step through choosing Spring Boot version, language, project type, and dependencies. It’s particularly useful for new team members or when you want to quickly prepare a project aligned with your organization’s standards.

WARNING
Interactive mode is still experimental — great for learning and quick start. In CI/CD pipelines I recommend explicit -P flags.

To use interactive mode, follow the instructions in the FAQ.


How it’s built

As mentioned earlier, besides using knowledge from DPE University, I wanted this plugin to be a reference implementation for building further plugins. My goal was to create something solid, nearly maintenance-free, and as timeless as possible.

plugin.properties

The heart of the plugin is the plugin.properties file, which contains default parameter values. This supports the principle of separating code and configuration - changes to values do not require code modification or recompilation, just file replacement.

Properties are loaded once at plugin startup and available through static constants:

1
PluginConfig.getOrThrow(PluginConstants.PLUGIN_ID)

This approach simplifies both production and test configuration – giving full control over the environment without changes to plugin logic.

Lessons from functional tests

For plugin.properties to be visible in functional tests as a resource, add to build.gradle:

1
2
3
4
5
sourceSets {
    functionalTest {
        resources.srcDir file('src/main/resources')
    }
}

This way functional tests use exactly the same configuration as the production plugin.

convention: clean plugin configuration

Options such as initializrUrl, metadataEndpoint and extract are designed as global plugin configuration – they concern the source of data (where to fetch the project from), not the project content itself (what it should include). This keeps separation between infrastructural configuration and actual generation logic.

Default values are set using convention(...) in Gradle - which means:

  • the user does not have to set anything to use the plugin in default mode,
  • but can override any value in build.gradle, for example to use their own Spring Initializr instance or disable ZIP extraction:
1
2
3
4
5
6
7
8
9
plugins {
    id 'io.oczadly.springinitializr' version '1.0.0'
}

tasks.named('initSpringBootProject') {
    initializrUrl = 'https://your-initializr.example.com'
    metadataEndpoint = '/yours'
    extract = 'false'
}

This approach provides:

  • clean builds – no mixing of configuration with invocation parameters.
  • predictability – the plugin always works on explicitly defined inputs.
  • flexibility – the user can point to a custom endpoint, e.g. in CI or a mirror.

See official Gradle documentation on convention(...) for more details on setting clear, overridable defaults in your plugins and tasks.

Extracting available options from Metadata API

The plugin fetches supported versions of Spring Boot, project types, and languages available after downloading from Spring Initializr. This is done via the Metadata endpoint. Thanks to this, there is no need to constantly update the plugin for supported parameters.

While adding supported project types and languages I encountered an interesting issue with configuration cache in Gradle. More on this below.

Lessons from using –configuration-cache

At first I ran into problems testing the plugin with the --configuration-cache flag. Initially I thought the issue was reading plugin.properties, but the real cause was using the project.* API in the class extending DefaultTask:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.oczadly.tasks

abstract class InitSpringBootProjectTask extends DefaultTask {

  @Internal
  final ListProperty<String> supportedProjectTypes = project.objects.listProperty String

  @Internal
  final ListProperty<String> supportedLanguages = project.objects.listProperty String

  @TaskAction
  void download() {
    /* Remaining code omitted for clarity */
  }
}

This breaks configuration cache, because Gradle requires all inputs to be explicitly declared using Property<T>, Provider<T> or DirectoryProperty. Then during execution phase you cannot refer to mutable project state.

I solved this by:

  • Moving all logic of fetching supported project types and languages into the download() method.
  • Ensuring that @TaskAction operates only on properties declared as Property<T> or Provider<T>, evaluated at execution time.

This change not only enabled configuration cache compatibility, but also led to a cleaner, more idiomatic implementation aligned with Gradle practices.

See official Gradle documentation on configuration cache for all details and limitations.

-P parameters and validation mechanism

The plugin accepts all parameters available at https://start.spring.io — the same ones you can set in the web UI of Spring Initializr.

After users set parameters or default values are assigned, validation is performed inside the initSpringBootProject task. At the beginning, the task fetches the list of supported Spring Boot versions, project types, and languages from the Metadata endpoint. If a user provides an invalid parameter value that is not supported, the task will fail. For example:

1
gradle initSpringBootProject -Planguage=clojure

Will result in the message:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
> Task :initSpringBootProject FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':initSpringBootProject'.
> Unsupported language: 'clojure'. Supported: java, kotlin, groovy.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 5s

Incremental build and build cache

Based on the Gradle Build Caching path at DPE University and the knowledge gained there, the plugin was given support for incremental build and Build Cache. Thanks to this, subsequent executions are instant, and results can be reused.

The initSpringBootProject task declares both inputs (@Input) and outputs (@OutputDirectory — in this case outputDir, tied to a DirectoryProperty). This is aligned with the definition of a correctly configured task in Gradle, described here.

Thanks to this, Gradle has full control over tracking the state of inputs and outputs, which enables:

  • Skipping the task (UP-TO-DATE) if the outputs have not changed — incremental build.
  • Restoring results (FROM-CACHE) if outputs exist in the local cache — build cache.

Below is an example of how the task behaves in both cases.

Incremental build

On the first run, the project is downloaded from Spring Initializr:

1
gradle initSpringBootProject -PoutputDir=/opt/my-projects/my-spring-boot-app --console=plain --info

You will see:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

...

Task name matched 'initSpringBootProject'
Selected primary task 'initSpringBootProject' from project :
Tasks to be executed: [task ':initSpringBootProject']
Tasks that were excluded: []
Resolve mutations for :initSpringBootProject (Thread[included builds,5,main]) started.
:initSpringBootProject (Thread[included builds,5,main]) started.

> Task :initSpringBootProject
Build cache key for task ':initSpringBootProject' is 75b4d5a65d989aa6453584fe39baeab5
Task ':initSpringBootProject' is not up-to-date because:
  No history is available.
Downloading Spring Boot starter project...
Project downloaded to: /opt/my-projects/my-spring-boot-app/starter.zip
Project extracted to: /opt/my-projects/my-spring-boot-app/demo
Stored cache entry for task ':initSpringBootProject' with cache key 75b4d5a65d989aa6453584fe39baeab5

BUILD SUCCESSFUL in 12s
5 actionable tasks: 1 executed, 4 up-to-date

On subsequent runs with the same -PoutputDir parameter:

1
gradle initSpringBootProject -PoutputDir=/opt/my-projects/my-spring-boot-app --console=plain --info

Since the outputs haven’t changed, you’ll see:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...

Task name matched 'initSpringBootProject'
Selected primary task 'initSpringBootProject' from project :
Tasks to be executed: [task ':initSpringBootProject']
Tasks that were excluded: []
Resolve mutations for :initSpringBootProject (Thread[Execution worker Thread 4,5,main]) started.
:initSpringBootProject (Thread[Execution worker Thread 4,5,main]) started.

> Task :initSpringBootProject UP-TO-DATE
Build cache key for task ':initSpringBootProject' is 75b4d5a65d989aa6453584fe39baeab5
Skipping task ':initSpringBootProject' as it is up-to-date.

BUILD SUCCESSFUL in 383ms
5 actionable tasks: 5 up-to-date

Which confirms that incremental build was used, marked with the UP-TO-DATE label.

Build cache

If the file /opt/my-projects/my-spring-boot-app/starter.zip is deleted and the task is run again:

1
gradle initSpringBootProject -PoutputDir=/opt/my-projects/my-spring-boot-app --console=plain --info

Since the outputs were previously added to the local cache, the response will show:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

...

Task name matched 'initSpringBootProject'
Selected primary task 'initSpringBootProject' from project :
Tasks to be executed: [task ':initSpringBootProject']
Tasks that were excluded: []
Resolve mutations for :initSpringBootProject (Thread[included builds,5,main]) started.
:initSpringBootProject (Thread[included builds,5,main]) started.

> Task :initSpringBootProject FROM-CACHE
Build cache key for task ':initSpringBootProject' is 75b4d5a65d989aa6453584fe39baeab5
Task ':initSpringBootProject' is not up-to-date because:
  Output property 'outputDir' file /opt/my-projects/my-spring-boot-app has been removed.
  Output property 'outputDir' file /opt/my-projects/my-spring-boot-app/demo has been removed.
  Output property 'outputDir' file /opt/my-projects/my-spring-boot-app/demo/build.gradle has been removed.
  and more...
Loaded cache entry for task ':initSpringBootProject' with cache key 75b4d5a65d989aa6453584fe39baeab5

BUILD SUCCESSFUL in 416ms
5 actionable tasks: 1 from cache, 4 up-to-date

This confirms that the local build cache was used, indicated by the FROM-CACHE label.

Lessons from the Gradle Build Cache path (DPE University

While going through the Gradle Build Caching path at DPE University, I became curious about how exactly the local build cache works and what actually goes inside it. I decided to break it down.

After running the initSpringBootProject task, the build cache key 75b4d5a65d989aa6453584fe39baeab5 stands out. That’s an archive located in $GRADLE_USER_HOME/caches/build-cache-1. When unpacked, you can see, among others, a METADATA file that contains:

1
2
3
4
5
6
7
8
9
#Generated origin information
#Sun Jul 13 13:47:46 CEST 2025
executionTime=787
creationTime=1752677283570
identity=\:initSpringBootProject
buildInvocationId=b6ynyhegwjcl5ojld4csg2g7ee
buildCacheKey=75b4d5a65d989aa6453584fe39baeab5
type=org.gradle.api.internal.tasks.execution.TaskExecution
gradleVersion=8.14

In the tree-outputDir/ folder there is the archived starter.zip file and the project folder — exactly what was previously downloaded from Spring Initializr and saved as the task’s outputs.

For more on incremental build and Build Cache, see the official documentation:

👉 Incremental build.

👉 Build Cache.

Interactive mode

To preserve compatibility with incremental build and build cache, upToDateWhen and cacheIf were used accordingly.


Production readiness

When building this plugin I wanted it to be ready for production use from day one. That means not just “it works on my machine”, but it is tested, validated, analyzable, and integrated with CI/CD.

Unit tests

Written in Spock. They cover key functionalities such as parameter validation, building queries to Spring Initializr, and extraction. In line with the testing pyramid, these are the most numerous tests in the project.

Functional tests

Also written in Spock, this time using Gradle TestKit. In addition, I test JSON responses from Spring Initializr with WireMock, following the official documentation recommended by the Spring Initializr team.

Lessons from using TestKit in Spock

At the beginning, while working on functional tests, I ran into a case described in the TestKit documentation in the section Controlling the build environment.

Even though the initSpringBootProject task executed successfully and in the logs I could see:

1
2
3
4
5
6
> Task :initSpringBootProject
Project downloaded to: /private/var/folders/lj/q3_9z0ws4ygb_bvng5vy2vs00000gn/T/spock_initSpringBootProje_0_testProjectDir13964729921630795147/build/generated-project/starter.zip
Project extracted to: /private/var/folders/lj/q3_9z0ws4ygb_bvng5vy2vs00000gn/T/spock_initSpringBootProje_0_testProjectDir13964729921630795147/build/generated-project/demo

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

The tests did not pass, and I was getting the following message in the logs:

1
2
3
4
5
6
Condition not satisfied:

FilesTestUtils.projectFilesExist unzipDir, 'build.gradle', 'src/main/java/com/example/demo/DemoApplication.java'
|              |                 |
|              false             /var/folders/lj/q3_9z0ws4ygb_bvng5vy2vs00000gn/T/spock_initSpringBootProje_0_testProjectDir8148397175690448479/generated-project/demo
class io.oczadly.testsupport.FilesTestUtils

The issue came from the fact that Gradle TestKit always runs the build in an isolated working directory inside java.io.tmpdir (e.g. /private/var/folders/.../build/), and not in the directory where I expected the files.

However, in the assertion (generatedProjectDir.absolutePath) it was looking for files in Spock’s temporary directory (@TempDir), which is a different location than the working directory used by TestKit.

I solved it by:

  • Setting -PoutputDir=${generatedProjectDir.absolutePath}, which forced the plugin to save files in the place expected by the test.

Static code analysis

Every tool released for use should include a mechanism for static code analysis. In this case, the CodeNarc plugin was used, with a necessary set of rules useful for tools like Gradle plugins.

Test coverage

As mentioned earlier, gradle-springinitializr-plugin is covered by both unit and functional tests. Coverage is measured using the JaCoCo plugin. It includes both types of tests and must exceed 80%.

Continuous integration

For continuous integration I used GitHub Actions. On every PR, it verifies whether the commit follows Conventional Commits. Then it runs gradle build, which checks the following:

  • Code compilation
  • Unit and functional tests
  • Static code analysis
  • Test coverage

Build Scan

At the end of the CI process, a Build Scan is published to https://scans.gradle.com. An example Build Scan from an execution can be found here.

Compatibility tests

While building the plugin, it was important to ensure compatibility with multiple versions of Gradle, as well as both Kotlin DSL and Groovy DSL. That’s why on every PR, in addition to the previously mentioned steps, compatibility tests are run. They execute the initSpringBootProject task on different versions of Gradle using the following directories:

  • examples/simple-groovy
  • examples/simple-kotlin

All of this is possible thanks to the matrix strategy in GitHub Actions. More about it can be found here.

Continuous delivery

After the CI process described above, when a commit lands in main, GitHub Actions triggers a workflow that uses semantic-release to manage changes in CHANGELOG.md, create a new tag with a release in the repository, and publish the plugin to Gradle - Plugins.


Summary

While building gradle-springinitializr-plugin, I not only enjoyed recreating gradle init for Spring Boot, going through DPE University courses, exploring the Spring Initializr API, Gradle best practices, Spock and Groovy. I am also fully satisfied with the final result. This is not just another sample plugin. It is a tool ready to use locally and as part of a platform — built consciously, solidly, and following the best patterns.

For Platform Engineers it’s a ready-to-use asset for the platform, for Software Engineers – a quick start in daily work, and for explorers – a practical reference for building Gradle plugins.

What’s next?

Interested in contributing to the plugin?

👉 See how you can get involved by checking the CONTRIBUTING file.

Have an idea for a new feature or found a bug?

👉 Submit your idea or report an issue here.

Do you like this blog and my work? Buy Me A Coffee