@@ -201,16 +201,60 @@ def projectDir = new File(request.outputDirectory)
201201def subDir = new File (projectDir, artifactId)
202202if (subDir. isDirectory()) projectDir = subDir
203203
204+ // ─── Spring dependency selection ──────────────────────────────────────────────
205+ // Each composed module declares the most specific Spring Boot starter that is useful
206+ // for its role rather than a blanket low-level dependency. common-domain (which every
207+ // other module depends on) carries the core spring-boot-starter, so the "plain" modules
208+ // — domain-* and the service modules — inherit the @Configuration / @ComponentScan API
209+ // transitively and declare no Spring dependency of their own. Presentation (server-side)
210+ // and integration (client-side) modules add a role-specific starter on top: a 'rest'
211+ // presentation serves endpoints via spring-boot-starter-web, while a 'rest' integration
212+ // consumes an external API via spring-boot-starter-restclient (no servlet web stack). All
213+ // gated on includeSpring; with Spring off no module references Spring at all.
214+
215+ def STARTER = [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter' ]
216+ def STARTER_WEB = [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-web' ]
217+ def STARTER_DATA_JPA = [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-data-jpa' ]
218+ def STARTER_GRAPHQL = [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-graphql' ]
219+ def STARTER_RESTCLIENT = [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-restclient' ]
220+
221+ /* * Role-specific starter for an integration (client-side) module, or null when none fits
222+ * (those modules rely on the core starter inherited transitively via common-domain). */
223+ def integrationStarter = { String type ->
224+ switch (type?. trim()?. toLowerCase()) {
225+ case ' database' : return STARTER_DATA_JPA
226+ case ' rest' : return STARTER_RESTCLIENT // consumes an external REST API; no servlet web stack
227+ case ' graphql' : return STARTER_GRAPHQL
228+ default : return null
229+ }
230+ }
231+
232+ /* * Role-specific starter for a presentation (server-side) module, or null when none fits. */
233+ def presentationStarter = { String type ->
234+ switch (type?. trim()?. toLowerCase()) {
235+ case ' rest' : return STARTER_WEB // serves REST endpoints over servlet MVC
236+ case ' graphql' : return STARTER_GRAPHQL
237+ default : return null
238+ }
239+ }
240+
241+ /* * Wraps a resolved starter into a module deps list: a one-element list when Spring is on
242+ * and a starter was resolved, otherwise empty. */
243+ def starterDeps = { starter ->
244+ (includeSpring && starter) ? [starter] : []
245+ }
246+
247+ // common-domain's own Spring dependency: the core starter, which provides the
248+ // @Configuration API to every dependent module transitively (empty when Spring is off).
249+ def commonSpring = includeSpring ? [STARTER ] : []
250+
204251// ─── Spring configuration class generator ─────────────────────────────────────
205252// When includeSpring is true, every module the app composes carries a @Configuration
206- // class in its `.config` sub-package (the app's Application class @Imports them all)
207- // and declares spring-context. The module root package also gets a marker
208- // "{Module}ComponentScan" interface that the @Configuration targets via
209- // @ComponentScan(basePackageClasses=…) — a type-safe anchor for scanning the whole
210- // module. Everything here is skipped when includeSpring is false.
211-
212- def SPRING_CONTEXT = [groupId : ' org.springframework' , artifactId : ' spring-context' ]
213- def springCtx = includeSpring ? [SPRING_CONTEXT ] : [] // appended to module deps only when Spring is enabled
253+ // class in its `.config` sub-package (the app's Application class @Imports them all).
254+ // The module root package also gets a marker "{Module}ComponentScan" interface that the
255+ // @Configuration targets via @ComponentScan(basePackageClasses=…) — a type-safe anchor
256+ // for scanning the whole module. Skipped entirely when includeSpring is false.
257+
214258def configFqns = []
215259
216260/* * Writes a {Module}ComponentScan marker interface into the module root package and a
@@ -279,7 +323,7 @@ def parentInherit = includeSpring ? """\
279323 <parent>
280324 <groupId>org.springframework.boot</groupId>
281325 <artifactId>spring-boot-starter-parent</artifactId>
282- <version>4.0.6 </version>
326+ <version>4.1.0 </version>
283327 <relativePath/>
284328 </parent>
285329
@@ -485,7 +529,7 @@ writeFile(projectDir, 'common-domain/pom.xml',
485529 modulePom(groupId, artifactId, version,
486530 ' common-domain' ,
487531 ' Domain classes common to all modules.' ,
488- springCtx ))
532+ commonSpring ))
489533srcTree(projectDir, ' common-domain' , pkgPath,
490534 [' common/domain' ], [' common/domain' ],
491535 ' Domain classes common to all modules.' )
@@ -515,7 +559,7 @@ integrations.each { intg ->
515559 modulePom(groupId, artifactId, version,
516560 domMod,
517561 " Domain classes for the ${ intg.name} ${ disp} data source." ,
518- [' common-domain' ] + springCtx ))
562+ [' common-domain' ]))
519563 srcTree(projectDir, domMod, pkgPath,
520564 [domSub], [domSub],
521565 " Domain classes for the ${ intg.name} ${ disp} integration." )
@@ -525,7 +569,7 @@ integrations.each { intg ->
525569 modulePom(groupId, artifactId, version,
526570 implMod,
527571 " Integration classes (DAOs, repositories) for the ${ intg.name} ${ disp} data source." ,
528- [' common-domain' , domMod] + springCtx ,
572+ [' common-domain' , domMod] + starterDeps(integrationStarter(intg . rawType)) ,
529573 failsafeSection))
530574 srcTree(projectDir, implMod, pkgPath,
531575 [implSub], [implSub],
@@ -551,15 +595,15 @@ serviceModules.each { svcMod ->
551595 writeFile(projectDir, " ${ domMod} /pom.xml" ,
552596 modulePom(groupId, artifactId, version,
553597 domMod, domDesc,
554- [' common-domain' ] + springCtx ))
598+ [' common-domain' ]))
555599 srcTree(projectDir, domMod, pkgPath, [domPkg], [domPkg], domDesc)
556600 configClass(domMod, domPkg)
557601
558602 // Business logic, depending on its own service-area domain module.
559603 writeFile(projectDir, " ${ svcMod} /pom.xml" ,
560604 modulePom(groupId, artifactId, version,
561605 svcMod, svcDesc,
562- [' common-domain' , domMod] + springCtx ))
606+ [' common-domain' , domMod]))
563607 srcTree(projectDir, svcMod, pkgPath, [svcPkg], [svcPkg], svcDesc)
564608 configClass(svcMod, svcPkg)
565609}
@@ -575,7 +619,7 @@ presentations.each { pType ->
575619 modulePom(groupId, artifactId, version,
576620 domMod,
577621 " Domain classes for the ${ disp} presentation tier." ,
578- [' common-domain' ] + springCtx ))
622+ [' common-domain' ]))
579623 srcTree(projectDir, domMod, pkgPath,
580624 [" domain/${ pType} " ], [" domain/${ pType} " ],
581625 " Domain classes for the ${ disp} presentation tier." )
@@ -585,7 +629,7 @@ presentations.each { pType ->
585629 modulePom(groupId, artifactId, version,
586630 implMod,
587631 " Request-handling classes (controllers) for the ${ disp} presentation tier." ,
588- [' common-domain' , domMod] + springCtx ))
632+ [' common-domain' , domMod] + starterDeps(presentationStarter(pType)) ))
589633 srcTree(projectDir, implMod, pkgPath,
590634 [" presentation/${ pType} " ], [" presentation/${ pType} " ],
591635 " Request-handling classes for the ${ disp} presentation tier." )
@@ -594,10 +638,19 @@ presentations.each { pType ->
594638
595639// ─── app ─────────────────────────────────────────────────────────────────────
596640
597- def appStarters = includeSpring ? [
598- [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-web' ],
599- [groupId : ' org.springframework.boot' , artifactId : ' spring-boot-starter-data-jpa' ],
600- ] : []
641+ // The app declares the web / data-jpa starters only when its composition warrants them, and
642+ // inherits whatever else its composed modules bring. A 'rest' presentation makes it a servlet
643+ // web app (so it also gets the WAR-deployment AppServletInitializer); a 'database' integration
644+ // gives it JPA + transactions (so Application carries @EnableTransactionManagement, which needs
645+ // spring-tx from spring-boot-starter-data-jpa). All gated on includeSpring.
646+ def hasRestPresentation = presentations. contains(' rest' )
647+ def hasDatabaseIntegration = integrations. any { it. rawType == ' database' }
648+
649+ def appStarters = []
650+ if (includeSpring) {
651+ if (hasRestPresentation) appStarters << STARTER_WEB
652+ if (hasDatabaseIntegration) appStarters << STARTER_DATA_JPA
653+ }
601654writeFile(projectDir, ' app/pom.xml' ,
602655 modulePom(groupId, artifactId, version,
603656 ' app' ,
@@ -611,22 +664,24 @@ if (includeSpring) {
611664// module's @Configuration class so the whole tree is assembled into one context.
612665def appConfigPkg = (pkgPath + ' /app/config' ). replace(' /' , ' .' )
613666def importsBlock = configFqns. sort(). collect { " ${ it} .class," }. join(' \n ' )
667+ // @EnableTransactionManagement requires spring-tx (pulled in by spring-boot-starter-data-jpa),
668+ // so it is only emitted when a database integration puts that starter on the app's classpath.
669+ def txImport = hasDatabaseIntegration ? ' import org.springframework.transaction.annotation.EnableTransactionManagement;\n ' : ' '
670+ def txAnnotation = hasDatabaseIntegration ? ' @EnableTransactionManagement\n ' : ' '
614671writeFile(projectDir, " app/src/main/java/${ pkgPath} /app/config/Application.java" , """ \
615672package ${ appConfigPkg} ;
616673
617674import org.springframework.beans.factory.annotation.Value;
618675import org.springframework.boot.SpringApplication;
619676import org.springframework.boot.autoconfigure.SpringBootApplication;
620677import org.springframework.context.annotation.Import;
621- import org.springframework.transaction.annotation.EnableTransactionManagement;
622-
678+ ${ txImport}
623679/**
624680 * Spring Boot main application class for running standalone in Boot configured embedded container. Not used with WAR
625681 * deployment.
626682 */
627683@SpringBootApplication
628- @EnableTransactionManagement
629- @Import({
684+ ${ txAnnotation} @Import({
630685${ importsBlock}
631686})
632687public class Application {
@@ -635,6 +690,9 @@ public class Application {
635690 }
636691}
637692""" )
693+ // AppServletInitializer extends SpringBootServletInitializer (a servlet web stack), so it is
694+ // only generated when a 'rest' presentation makes the app a servlet web application.
695+ if (hasRestPresentation) {
638696writeFile(projectDir, " app/src/main/java/${ pkgPath} /app/config/AppServletInitializer.java" , """ \
639697package ${ appConfigPkg} ;
640698
@@ -657,6 +715,7 @@ public class AppServletInitializer extends SpringBootServletInitializer {
657715}
658716""" )
659717}
718+ }
660719
661720// ─── acceptance-tests ─────────────────────────────────────────────────────────
662721
0 commit comments