diff --git a/gradle/plugins/config/checkstyle/checkstyle.xml b/gradle/plugins/config/checkstyle/checkstyle.xml
new file mode 100644
index 00000000000..1ad50d8fcb8
--- /dev/null
+++ b/gradle/plugins/config/checkstyle/checkstyle.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/plugins/cycle-detection-plugin/build.gradle b/gradle/plugins/cycle-detection-plugin/build.gradle
new file mode 100644
index 00000000000..51de7cb5dca
--- /dev/null
+++ b/gradle/plugins/cycle-detection-plugin/build.gradle
@@ -0,0 +1,30 @@
+plugins {
+ id 'java-gradle-plugin'
+ id "checkstyle"
+ id "io.spring.javaformat" version "$javaFormatVersion"
+
+}
+
+repositories {
+ mavenCentral()
+}
+
+checkstyle {
+ toolVersion = "${checkstyleToolVersion}"
+}
+
+dependencies {
+ checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}")
+ checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}")
+
+ implementation("org.jgrapht:jgrapht-core:1.5.2")
+}
+
+gradlePlugin {
+ plugins {
+ cycleDetectionPlugin {
+ id = "org.springframework.boot.cycle-detection"
+ implementationClass = "org.springframework.boot.build.cycledetection.CycleDetectionPlugin"
+ }
+ }
+}
diff --git a/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java b/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java
new file mode 100644
index 00000000000..84482819702
--- /dev/null
+++ b/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.build.cycledetection;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.gradle.api.GradleException;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.execution.TaskExecutionGraph;
+import org.gradle.api.initialization.Settings;
+import org.jgrapht.Graph;
+import org.jgrapht.alg.cycle.TarjanSimpleCycles;
+import org.jgrapht.graph.DefaultDirectedGraph;
+import org.jgrapht.graph.DefaultEdge;
+
+/**
+ * A {@link Settings} {@link Plugin plugin} to detect cycles between a build's projects.
+ *
+ * @author Andy Wilkinson
+ */
+public class CycleDetectionPlugin implements Plugin {
+
+ @Override
+ public void apply(Settings settings) {
+ settings.getGradle().getTaskGraph().whenReady(this::detectCycles);
+ }
+
+ private void detectCycles(TaskExecutionGraph taskGraph) {
+ Map> dependenciesByProject = getProjectsAndDependencies(taskGraph);
+ Graph graph = createGraph(dependenciesByProject);
+ List> cycles = findCycles(graph);
+ if (!cycles.isEmpty()) {
+ StringBuilder message = new StringBuilder("Cycles detected:\n");
+ for (List cycle : cycles) {
+ cycle.add(cycle.get(0));
+ message.append(" " + String.join(" -> ", cycle) + "\n");
+ }
+ throw new GradleException(message.toString());
+ }
+ }
+
+ private Map> getProjectsAndDependencies(TaskExecutionGraph taskGraph) {
+ Map> dependenciesByProject = new HashMap<>();
+ for (Task task : taskGraph.getAllTasks()) {
+ Project project = task.getProject();
+ Set dependencies = dependenciesByProject.computeIfAbsent(project, (p) -> new LinkedHashSet<>());
+ taskGraph.getDependencies(task)
+ .stream()
+ .map(Task::getProject)
+ .filter((taskProject) -> !taskProject.equals(project))
+ .forEach(dependencies::add);
+ }
+ return dependenciesByProject;
+ }
+
+ private Graph createGraph(Map> dependenciesByProject) {
+ Graph graph = new DefaultDirectedGraph<>(DefaultEdge.class);
+ dependenciesByProject.keySet().forEach((project) -> graph.addVertex(project.getName()));
+ dependenciesByProject.forEach((project, dependencies) -> dependencies
+ .forEach((dependency) -> graph.addEdge(project.getName(), dependency.getName())));
+ return graph;
+ }
+
+ private List> findCycles(Graph graph) {
+ TarjanSimpleCycles simpleCycles = new TarjanSimpleCycles<>(graph);
+ List> cycles = simpleCycles.findSimpleCycles();
+ return cycles;
+ }
+
+}
diff --git a/gradle/plugins/settings.gradle b/gradle/plugins/settings.gradle
new file mode 100644
index 00000000000..31c79396773
--- /dev/null
+++ b/gradle/plugins/settings.gradle
@@ -0,0 +1,12 @@
+pluginManagement {
+ new File(rootDir.parentFile.parentFile, "gradle.properties").withInputStream {
+ def properties = new Properties()
+ properties.load(it)
+ properties.forEach(settings.ext::set)
+ gradle.rootProject {
+ properties.forEach(project.ext::set)
+ }
+ }
+}
+
+include 'cycle-detection-plugin'
diff --git a/settings.gradle b/settings.gradle
index bb6851f0388..2d9d18ef35a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -15,10 +15,12 @@ pluginManagement {
}
}
}
+ includeBuild("gradle/plugins")
}
plugins {
id "io.spring.develocity.conventions" version "0.0.22"
+ id "org.springframework.boot.cycle-detection"
}
rootProject.name="spring-boot-build"