Skip to content

Commit f444d95

Browse files
jeffjensenclaude
andcommitted
feat(arch): Add per-service-area domain-service modules
Each service area now owns a dedicated domain module, separating its business logic from its domain classes: - For every `service` / `service-{name}` area, also generate a `domain-service` / `domain-service-{name}` module (package `{base.package}.domain.service[.{name}]`) that holds the area's domain classes and depends only on `common-domain`. - The `service` / `service-{name}` module now depends on its own domain-service module in addition to `common-domain`. - `acceptance-tests` depends on the new service-area domain modules alongside the integration and presentation domain modules. This also refines the generated scaffold and its documentation: - `common-testing` becomes shared test infrastructure and gains a `common/testing` package under src/test/java; `acceptance-tests` gains an `at` package under src/main/java. - Add `typeDisplay` (e.g. GraphQL) and `lcFirst` helpers; normalize pom <description> and package-info Javadoc wording (trailing periods, display names) and prefix test-package Javadoc with "Tests for ...". - Update the Maven site docs (modules.adoc, usage.adoc), requirements.adoc, and the affected verify.groovy assertions to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTaQTM9wYTXLvvgCXSwhRm
1 parent 5ba1b75 commit f444d95

18 files changed

Lines changed: 188 additions & 73 deletions

File tree

src/main/resources/META-INF/archetype-post-generate.groovy

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ def typeAbbrev = { String t ->
99
t.trim().toLowerCase() == 'database' ? 'db' : t.trim().toLowerCase()
1010
}
1111

12+
/** Returns the human-readable display name used in prose (Javadoc, pom descriptions) for a type. */
13+
def typeDisplay = { String t ->
14+
def s = t.trim().toLowerCase()
15+
s == 'graphql' ? 'GraphQL' : s
16+
}
17+
18+
/** Lower-cases the first character of a string, used to splice a description into a sentence. */
19+
def lcFirst = { String s ->
20+
(s == null || s.isEmpty()) ? s : Character.toLowerCase(s.charAt(0)).toString() + s.substring(1)
21+
}
22+
1223
def ensureDir = { File base, String rel ->
1324
// File(parent, child) normalises separators on all platforms
1425
new File(base, rel).mkdirs()
@@ -37,13 +48,15 @@ package ${fullPkg};
3748
}
3849
ensureDir(base, "${module}/src/main/resources")
3950
ensureDir(base, "${module}/src/test/java")
51+
// Test-package Javadoc reuses the main wording, prefixed to mark it as the tests for that code.
52+
def testDesc = 'Tests for ' + lcFirst(description.replaceAll(/\.\s*$/, '')) + '.'
4053
testSubs.each { sub ->
4154
def fullPkg = (pkgPath + '/' + sub).replace('/', '.')
4255
def dir = "${module}/src/test/java/${pkgPath}/${sub}"
4356
ensureDir(base, dir)
4457
writeFile(base, "${dir}/package-info.java", """\
4558
/**
46-
* ${description}
59+
* ${testDesc}
4760
*/
4861
package ${fullPkg};
4962
""")
@@ -149,21 +162,22 @@ def presentations = presentationTypesInput
149162

150163
def intDomainMods = integrations.collect { "domain-${it.abbrev}-${it.name}" }
151164
def intImplMods = integrations.collect { "integration-${it.abbrev}-${it.name}" }
165+
def svcDomainMods = serviceModules.collect { "domain-${it}" }
152166
def presDomainMods = presentations.collect { "domain-${it}" }
153167
def presImplMods = presentations.collect { "presentation-${it}" }
154168

155169
def allModules = (
156170
['acceptance-tests', 'app', 'common-domain', 'common-testing'] +
157171
intDomainMods + intImplMods +
158-
serviceModules +
172+
serviceModules + svcDomainMods +
159173
presDomainMods + presImplMods
160174
).sort()
161175

162176
// app depends on all non-test, non-self modules
163177
def appDeps = allModules.findAll { it != 'acceptance-tests' && it != 'common-testing' && it != 'app' }
164178

165-
// acceptance-tests depends on app + all domain modules
166-
def atDeps = (['app', 'common-domain'] + intDomainMods + presDomainMods).sort().unique()
179+
// acceptance-tests depends on app + all domain modules (including each service area's domain module)
180+
def atDeps = (['app', 'common-domain'] + intDomainMods + svcDomainMods + presDomainMods).sort().unique()
167181

168182
// ─── Resolve Project Root ─────────────────────────────────────────────────────
169183

@@ -386,7 +400,7 @@ try {
386400
writeFile(projectDir, 'common-domain/pom.xml',
387401
modulePom(groupId, artifactId, version,
388402
'common-domain',
389-
'Domain classes common to all modules'))
403+
'Domain classes common to all modules.'))
390404
srcTree(projectDir, 'common-domain', pkgPath,
391405
['common/domain'], ['common/domain'],
392406
'Domain classes common to all modules.')
@@ -396,54 +410,67 @@ srcTree(projectDir, 'common-domain', pkgPath,
396410
writeFile(projectDir, 'common-testing/pom.xml',
397411
modulePom(groupId, artifactId, version,
398412
'common-testing',
399-
'Testing utilities common to all test modules',
413+
'Testing infrastructure common to all test modules.',
400414
['common-domain']))
401415
srcTree(projectDir, 'common-testing', pkgPath,
402-
['common/testing'], [],
403-
'Test utilities common to all modules.')
416+
['common/testing'], ['common/testing'],
417+
'Test infrastructure common to all modules.')
404418

405419
// ─── Integration: domain-{type}-{name} and integration-{type}-{name} ──────────
406420

407421
integrations.each { intg ->
408422
def domMod = "domain-${intg.abbrev}-${intg.name}"
409423
def implMod = "integration-${intg.abbrev}-${intg.name}"
424+
def disp = typeDisplay(intg.rawType)
425+
def domSub = "domain/${intg.abbrev}/${intg.name}"
426+
def implSub = "integration/${intg.abbrev}/${intg.name}"
410427

411428
writeFile(projectDir, "${domMod}/pom.xml",
412429
modulePom(groupId, artifactId, version,
413430
domMod,
414-
"Domain classes for ${intg.rawType} data source: ${intg.name}",
431+
"Domain classes for the ${intg.name} ${disp} data source.",
415432
['common-domain']))
416433
srcTree(projectDir, domMod, pkgPath,
417-
["domain/${intg.abbrev}/${intg.name}"],
418-
["domain/${intg.abbrev}/${intg.name}"],
419-
"Domain classes for the ${intg.name} ${intg.rawType} integration.")
434+
[domSub], [domSub],
435+
"Domain classes for the ${intg.name} ${disp} integration.")
420436

421437
writeFile(projectDir, "${implMod}/pom.xml",
422438
modulePom(groupId, artifactId, version,
423439
implMod,
424-
"Integration classes (DAOs, repositories) for ${intg.rawType}: ${intg.name}",
440+
"Integration classes (DAOs, repositories) for the ${intg.name} ${disp} data source.",
425441
['common-domain', domMod],
426442
failsafeSection))
427443
srcTree(projectDir, implMod, pkgPath,
428-
["integration/${intg.abbrev}/${intg.name}"],
429-
["integration/${intg.abbrev}/${intg.name}"],
430-
"Integration classes for the ${intg.name} ${intg.rawType} data source.")
444+
[implSub], [implSub],
445+
"Integration classes for the ${intg.name} ${disp} data source.")
431446
}
432447

433448
// ─── Service modules ──────────────────────────────────────────────────────────
434449

435450
serviceModules.each { svcMod ->
436451
def label = svcMod == 'service' ? 'primary' : svcMod.replaceFirst('service-', '')
437452
def svcPkg = svcMod == 'service' ? 'service'
438-
: "service/${svcMod.replaceFirst('service-', '')}"
453+
: "service/${label}"
454+
def domMod = "domain-${svcMod}"
455+
def domPkg = "domain/${svcPkg}"
439456

440-
writeFile(projectDir, "${svcMod}/pom.xml",
441-
modulePom(groupId, artifactId, version,
442-
svcMod,
443-
"Business logic for the ${label} service area",
444-
['common-domain']))
445457
def svcDesc = svcMod == 'service' ? 'Business logic for the application.'
446458
: "Business logic for the ${label} service area."
459+
def domDesc = svcMod == 'service' ? 'Domain classes for the application.'
460+
: "Domain classes for the ${label} service area."
461+
462+
// Domain classes owned by this service area.
463+
writeFile(projectDir, "${domMod}/pom.xml",
464+
modulePom(groupId, artifactId, version,
465+
domMod, domDesc,
466+
['common-domain']))
467+
srcTree(projectDir, domMod, pkgPath, [domPkg], [domPkg], domDesc)
468+
469+
// Business logic, depending on its own service-area domain module.
470+
writeFile(projectDir, "${svcMod}/pom.xml",
471+
modulePom(groupId, artifactId, version,
472+
svcMod, svcDesc,
473+
['common-domain', domMod]))
447474
srcTree(projectDir, svcMod, pkgPath, [svcPkg], [svcPkg], svcDesc)
448475
}
449476

@@ -452,32 +479,33 @@ serviceModules.each { svcMod ->
452479
presentations.each { pType ->
453480
def domMod = "domain-${pType}"
454481
def implMod = "presentation-${pType}"
482+
def disp = typeDisplay(pType)
455483

456484
writeFile(projectDir, "${domMod}/pom.xml",
457485
modulePom(groupId, artifactId, version,
458486
domMod,
459-
"Domain classes for ${pType} presentation tier",
487+
"Domain classes for the ${disp} presentation tier.",
460488
['common-domain']))
461489
srcTree(projectDir, domMod, pkgPath,
462490
["domain/${pType}"], ["domain/${pType}"],
463-
"Domain classes for the ${pType} presentation tier.")
491+
"Domain classes for the ${disp} presentation tier.")
464492

465493
writeFile(projectDir, "${implMod}/pom.xml",
466494
modulePom(groupId, artifactId, version,
467495
implMod,
468-
"Request handling classes (controllers) for ${pType}",
496+
"Request-handling classes (controllers) for the ${disp} presentation tier.",
469497
['common-domain', domMod]))
470498
srcTree(projectDir, implMod, pkgPath,
471499
["presentation/${pType}"], ["presentation/${pType}"],
472-
"Request-handling classes for the ${pType} presentation tier.")
500+
"Request-handling classes for the ${disp} presentation tier.")
473501
}
474502

475503
// ─── app ─────────────────────────────────────────────────────────────────────
476504

477505
writeFile(projectDir, 'app/pom.xml',
478506
modulePom(groupId, artifactId, version,
479507
'app',
480-
'Application assembly — produces the runnable artifact',
508+
'Application assembly — produces the runnable artifact.',
481509
appDeps))
482510
srcTree(projectDir, 'app', pkgPath, ['app'], ['app'],
483511
'Application assembly and entry point.')
@@ -487,8 +515,8 @@ srcTree(projectDir, 'app', pkgPath, ['app'], ['app'],
487515
writeFile(projectDir, 'acceptance-tests/pom.xml',
488516
modulePom(groupId, artifactId, version,
489517
'acceptance-tests',
490-
'Functional acceptance tests for the application',
518+
'Functional acceptance tests for the application.',
491519
atDeps,
492520
failsafeSection))
493-
srcTree(projectDir, 'acceptance-tests', pkgPath, [], ['at'],
521+
srcTree(projectDir, 'acceptance-tests', pkgPath, ['at'], ['at'],
494522
'Functional acceptance tests for the application.')

src/site/asciidoc/modules.adoc

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
Every module contains a full `src/` scaffold
77
(`src/main/java`, `src/main/resources`, `src/test/java`, `src/test/resources`)
8-
and a `package-info.java` in every applicable leaf package directory.
8+
with a `package-info.java` in each module's primary leaf package under both
9+
`src/main/java` and `src/test/java`.
910

1011
== Fixed Modules
1112

@@ -17,7 +18,7 @@ Domain classes shared across all other modules. No dependencies on other generat
1718
_Package_: `{base.package}.common.domain`
1819

1920
`common-testing`::
20-
Shared test utilities. Has no test-scope Java packages.
21+
Shared test infrastructure.
2122
+
2223
_Package_: `{base.package}.common.testing` +
2324
_Depends on_: `common-domain`
@@ -26,21 +27,27 @@ _Depends on_: `common-domain`
2627
Business logic for a service area (see the `serviceAreas` prompt).
2728
+
2829
_Package_: `{base.package}.service` or `{base.package}.service.{name}` +
30+
_Depends on_: `common-domain`, its own `domain-service` / `domain-service-{name}` module
31+
32+
`domain-service` _or_ `domain-service-{name}`::
33+
Domain classes owned by a service area — one per `service` / `service-{name}` module.
34+
+
35+
_Package_: `{base.package}.domain.service` or `{base.package}.domain.service.{name}` +
2936
_Depends on_: `common-domain`
3037

3138
`app`::
3239
Application assembly — produces the runnable artifact (e.g. Spring Boot über-jar).
3340
+
3441
_Package_: `{base.package}.app` +
3542
_Depends on_: all non-test modules (`common-domain`, all domain and integration modules,
36-
all service modules, all presentation modules)
43+
all service modules and their `domain-service` modules, all presentation modules)
3744

3845
`acceptance-tests`::
3946
Functional acceptance tests that exercise the application from the outside.
40-
Has no main-scope Java packages.
4147
+
4248
_Package_: `{base.package}.at` +
4349
_Depends on_: `app`, `common-domain`, all `domain-{type}-{name}` integration domain modules,
50+
all `domain-service` / `domain-service-{name}` service-area domain modules,
4451
all `domain-{type}` presentation domain modules +
4552
_Plugin_: `maven-failsafe-plugin` (`integration-test` + `verify` goals)
4653

@@ -148,6 +155,8 @@ All type and name segments in package names are lowercased.
148155
|`integration-{type}-{name}` |`{base.package}.integration.{type}.{name}`
149156
|`service` |`{base.package}.service`
150157
|`service-{name}` |`{base.package}.service.{name}`
158+
|`domain-service` |`{base.package}.domain.service`
159+
|`domain-service-{name}` |`{base.package}.domain.service.{name}`
151160
|`domain-{type}` |`{base.package}.domain.{type}`
152161
|`presentation-{type}` |`{base.package}.presentation.{type}`
153162
|`app` |`{base.package}.app`

src/site/asciidoc/requirements.adoc

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,9 @@ Shared domain classes. No dependencies on other generated modules.
8080

8181
=== `common-testing`
8282

83-
Shared test utilities.
83+
Shared test infrastructure.
8484

8585
* Dependency: `common-domain`
86-
* No test-scope Java packages (the `src/test/java` directory is created but contains no source files).
8786

8887
=== Per integration: `domain-{type}-{name}` and `integration-{type}-{name}`
8988

@@ -100,7 +99,12 @@ For each `type:name` integration entry, generate two modules
10099
=== Per service area: `service` or `service-{name}`
101100

102101
Business logic for a service area.
103-
Dependency: `common-domain`.
102+
Dependencies: `common-domain` and the area's own `domain-service` /
103+
`domain-service-{name}` module.
104+
105+
For each service area, also generate a `domain-service` (single service) or
106+
`domain-service-{name}` module holding that area's domain classes
107+
(dependency: `common-domain`).
104108

105109
=== Per presentation tier: `domain-{type}` and `presentation-{type}`
106110

@@ -121,19 +125,20 @@ and `app` itself.
121125
=== `acceptance-tests`
122126

123127
Functional tests that exercise the application end-to-end.
124-
No main-scope Java packages (the `src/main/java` directory is created but contains no source files).
125128

126129
* Dependencies: `app`, `common-domain`, all `domain-{type}-{name}` modules,
130+
all `domain-service` / `domain-service-{name}` modules,
127131
all `domain-{type}` (presentation) modules.
128-
* Do not depend on service modules or integration implementation modules.
132+
* Do not depend on service (business-logic) modules or integration implementation modules.
129133
* Configure `maven-failsafe-plugin` with the `integration-test` and `verify` goals.
130134

131135
[[source-structure]]
132136
== Source Structure
133137

134138
Each module's source root is the package derived from the base package prompt.
135-
Place a `package-info.java` in each leaf package directory with a Javadoc comment
136-
describing the package's purpose.
139+
Place a `package-info.java` in each module's primary leaf package under both
140+
`src/main/java` and `src/test/java`, with a Javadoc comment describing the package's
141+
purpose; the test-side comment is prefixed with "Tests for ...".
137142
The package for each module is:
138143

139144
[cols="1,1"]
@@ -146,6 +151,8 @@ The package for each module is:
146151
|`integration-{type}-{name}` |`${base.package}.integration.{type}.{name}`
147152
|`service` |`${base.package}.service`
148153
|`service-{name}` |`${base.package}.service.{name}`
154+
|`domain-service` |`${base.package}.domain.service`
155+
|`domain-service-{name}` |`${base.package}.domain.service.{name}`
149156
|`domain-{type}` |`${base.package}.domain.{type}`
150157
|`presentation-{type}` |`${base.package}.presentation.{type}`
151158
|`app` |`${base.package}.app`

src/site/asciidoc/usage.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`, `service
105105
|===
106106

107107
Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`,
108-
`domain-db-users`, `domain-rest`, `integration-db-users`, `presentation-rest`, `service`.
108+
`domain-db-users`, `domain-rest`, `domain-service`,
109+
`integration-db-users`, `presentation-rest`, `service`.
109110

110111
=== Full multi-service project
111112

@@ -121,6 +122,7 @@ Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`,
121122
Produces: `acceptance-tests`, `app`, `common-domain`, `common-testing`,
122123
`domain-db-users`, `domain-graphql`, `domain-graphql-catalog`,
123124
`domain-rest`, `domain-rest-orders`,
125+
`domain-service-inventory`, `domain-service-orders`,
124126
`integration-db-users`, `integration-graphql-catalog`, `integration-rest-orders`,
125127
`presentation-graphql`, `presentation-rest`,
126128
`service-inventory`, `service-orders`.

src/test/resources/projects/basic/verify.groovy

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ assert !new File(basedir, 'pom.xml').exists() : "Root pom.xml must not exist; pa
2828

2929
def modules = [
3030
'acceptance-tests', 'app', 'common-domain', 'common-testing',
31+
'domain-service',
3132
'domain-db-main', 'domain-rest', 'integration-db-main',
3233
'presentation-rest', 'service'
3334
]
@@ -161,11 +162,11 @@ assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains
161162
assert content.startsWith('/**') : "${path} must start with a Javadoc comment"
162163
}
163164

164-
// common-testing has no test Java packages; acceptance-tests has no main Java packages
165-
assert !new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() \
166-
: 'common-testing must not have test Java package-info.java'
167-
assert !new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() \
168-
: 'acceptance-tests must not have main Java package-info.java'
165+
// common-testing and acceptance-tests now carry package-info in both source roots
166+
assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() \
167+
: 'common-testing must have test Java package-info.java'
168+
assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() \
169+
: 'acceptance-tests must have main Java package-info.java'
169170

170171
// ── 8. Module dependency wiring ───────────────────────────────────────────────
171172

@@ -239,5 +240,11 @@ def psScript = text('parent/mvn-update-properties-versions.ps1')
239240
assert psScript.contains('mvn versions:update-properties') : 'PowerShell script must run mvn versions:update-properties'
240241
assert psScript.contains('\r\n') : 'PowerShell script must use CRLF line endings'
241242

243+
// ── #8 service-area domain module ────────────────────────────────────────────
244+
245+
check('domain-service/pom.xml')
246+
assert text('domain-service/pom.xml').contains('<artifactId>common-domain</artifactId>') : 'domain-service missing common-domain dep'
247+
assert text('service/pom.xml').contains('<artifactId>domain-service</artifactId>') : 'service must depend on domain-service'
248+
242249
_buildLog.append("\n=== PASSED ===\n")
243250
true

src/test/resources/projects/blank-presentation-types/verify.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ assert !new File(basedir, 'pom.xml').exists() : "Root pom.xml must not exist"
3030

3131
def modules = [
3232
'acceptance-tests', 'app', 'common-domain', 'common-testing',
33+
'domain-service',
3334
'domain-db-main', 'integration-db-main', 'service',
3435
]
3536
modules.each { m -> check("${m}/pom.xml") }
@@ -93,8 +94,8 @@ assert text("common-testing/src/main/java/${p}/common/testing/package-info.java"
9394
assert text("service/src/main/java/${p}/service/package-info.java").contains("package ${pkg}.service;") : 'service package-info.java has wrong package declaration'
9495
assert text("app/src/main/java/${p}/app/package-info.java").contains("package ${pkg}.app;") : 'app package-info.java has wrong package declaration'
9596
assert text("acceptance-tests/src/test/java/${p}/at/package-info.java").contains("package ${pkg}.at;") : 'acceptance-tests package-info.java has wrong package declaration'
96-
assert !new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must not have test Java package-info.java'
97-
assert !new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must not have main Java package-info.java'
97+
assert new File(basedir, "common-testing/src/test/java/${p}/common/testing/package-info.java").exists() : 'common-testing must have test Java package-info.java'
98+
assert new File(basedir, "acceptance-tests/src/main/java/${p}/at/package-info.java").exists() : 'acceptance-tests must have main Java package-info.java'
9899

99100
// ── 7. Dependency wiring ─────────────────────────────────────────────────────
100101

0 commit comments

Comments
 (0)