Skip to content

Commit a5bedf3

Browse files
committed
feat(multi-module): restructure into Maven reactor with core and optional feature modules
1 parent 8e6808b commit a5bedf3

4 files changed

Lines changed: 497 additions & 0 deletions

File tree

docs/TODO.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,13 @@ wie bisher. Zwei OIDC-URLs pro Umgebung zu pflegen.
115115

116116
(b) Indicator auf jwk-set-uri umstellen — pingt direkt den JWKS-Endpoint. Eine Config-URL. Verliert den Check, ob das
117117
Discovery-Dokument valide ist (für uns als Resource-Server fachlich aber irrelevant — wir brauchen nur JWKS).
118+
119+
- **Spring Modulith für echte Modulgrenzen evaluieren.** Nach dem Multi-Modul-Umbau (Spec 014) sind die
120+
Modulgrenzen reine Konvention: plain Maven-Classpath-Module ohne JPMS (bewusst gewählt, da Spring sich schlecht
121+
mit JPMS verträgt), d.h. jeder `public` Typ in `spring-services-core` ist modulübergreifend erreichbar. Spring
122+
Modulith könnte die fachlichen Spring-Modulgrenzen explizit deklarieren und per `ApplicationModules`-Test
123+
verifizieren (erlaubte Abhängigkeiten, keine Zugriffe auf interne Pakete).
124+
**Context:** Surfaced in der `/grill-me`-Session zu Spec 014 (Multi-Modul-Umbau), Branch E (API-Oberfläche zwischen
125+
Modulen) — JPMS wurde verworfen, Modulith als Alternative geparkt.
126+
**Prerequisite:** Spec 014 (Multi-Modul-Umbau) muss zuerst landen.
118127

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Behaviors: Multi-module restructuring
2+
3+
These scenarios are largely **build- and wiring-level** (Maven reactor, classpath composition, Spring
4+
auto-configuration) rather than HTTP request/response behaviors. They are written so each can be
5+
turned into a reactor build assertion, a Maven dependency/classpath check, or a Spring integration
6+
test (`ApplicationContextRunner` / `@SpringBootTest`).
7+
8+
## Reactor build
9+
10+
### Reactor builds all modules
11+
12+
- **Given** the multi-module reactor (`spring-services` parent + `core`, `slack`, `mcp`, `email`,
13+
`search`, `dbbackup`, `all`, `bom`)
14+
- **When** `mvn verify` runs at the reactor root
15+
- **Then** every module builds and its tests pass, and the build produces a jar for each non-pom
16+
module plus pom artifacts for the parent and the BOM
17+
18+
### Parent coordinate is no longer a consumable jar
19+
20+
- **Given** the repurposed `com.open-elements:spring-services` coordinate
21+
- **When** its packaging is inspected
22+
- **Then** it is `pom` (reactor parent) and produces no class-bearing jar
23+
24+
### Lockstep version across all modules
25+
26+
- **Given** the reactor after a global version change via `versions:set`
27+
- **When** the version of each module and the BOM is read
28+
- **Then** all modules and the BOM carry the identical version, and each module references the parent
29+
version explicitly
30+
31+
## Dependency isolation
32+
33+
### Core does not pull optional heavy dependencies
34+
35+
- **Given** `spring-services-core` only
36+
- **When** its resolved transitive runtime classpath is computed
37+
- **Then** it contains none of `slack-api-client`, the MCP SDK artifacts, or `spring-boot-starter-mail`
38+
39+
### À-la-carte consumer pulls only chosen features
40+
41+
- **Given** a consumer depending on `spring-services-core` + `spring-services-slack` only
42+
- **When** the consumer's transitive classpath is computed
43+
- **Then** `slack-api-client` is present, but the MCP SDK, mail, and the search/dbbackup modules are
44+
absent
45+
46+
### BOM resolves all module versions from one import
47+
48+
- **Given** a consumer that imports `spring-services-bom` (scope `import`) and declares
49+
`spring-services-core` and `spring-services-mcp` **without** versions
50+
- **When** dependency resolution runs
51+
- **Then** both modules resolve at the BOM's lockstep version
52+
53+
### all-module brings the complete set
54+
55+
- **Given** a consumer depending only on `spring-services-all`
56+
- **When** its transitive classpath is computed
57+
- **Then** every spring-services module and every feature dependency (slack, mcp, mail, …) is present
58+
59+
## Auto-configuration activation
60+
61+
### Full classpath activates every feature
62+
63+
- **Given** an application context with `spring-services-all` on the classpath and all feature
64+
properties enabled
65+
- **When** the context starts
66+
- **Then** the core beans and every optional feature's beans (Slack, MCP, email, search, db-backup)
67+
are present — equivalent to today's `@Import(FullSpringServiceConfig)` behavior
68+
69+
### Absent optional module does not break startup
70+
71+
- **Given** an application with `spring-services-core` only (no optional modules)
72+
- **When** the context starts
73+
- **Then** it starts successfully, exposes the core beans, and no `NoClassDefFoundError` or missing-bean
74+
failure occurs for any absent optional feature
75+
76+
### Optional module self-activates by classpath presence
77+
78+
- **Given** an application with `spring-services-core` + `spring-services-mcp` on the classpath and
79+
`openelements.mcp.enabled=true`
80+
- **When** the context starts
81+
- **Then** the MCP auto-configuration activates and the MCP server beans are present, without the
82+
application declaring any `@Import`
83+
84+
### Optional module stays inert when its class is missing
85+
86+
- **Given** the `spring-services-mcp` auto-configuration class on the classpath but the MCP SDK type
87+
it guards on absent
88+
- **When** the context starts (simulated via `ApplicationContextRunner` without the SDK)
89+
- **Then** the `@ConditionalOnClass` guard prevents activation and no MCP bean is created
90+
91+
### Per-feature property toggle still disables a present module
92+
93+
- **Given** an application with `spring-services-mcp` present but `openelements.mcp.enabled=false`
94+
- **When** the context starts
95+
- **Then** the MCP endpoint and beans are not registered (existing `@ConditionalOnProperty` behavior is
96+
preserved)
97+
98+
### all-module ships no aggregate configuration
99+
100+
- **Given** `spring-services-all`
101+
- **When** its artifact contents are inspected
102+
- **Then** it contains no `@Configuration`/`@AutoConfiguration` class and no
103+
`AutoConfiguration.imports` of its own — it is a pure dependency bundle
104+
105+
## Persistence unit (carried over from spec 013)
106+
107+
### Single persistence unit resolves all entities
108+
109+
- **Given** an application with `spring-services-core` (entities in `oe_spring_services`) and its own
110+
`@Entity` in a separate package, with no `@Import`/`@EntityScan`/`@EnableJpaRepositories`
111+
- **When** the context starts
112+
- **Then** both the application's and the library's entities and Spring Data repositories resolve under
113+
one `EntityManagerFactory` / one `TransactionManager`
114+
115+
### Library-internal foreign keys and transactions still work
116+
117+
- **Given** the core module against a Testcontainers PostgreSQL
118+
- **When** an operation writes across library-internal associations (e.g. `audit_log.user_id → users.id`)
119+
within one transaction
120+
- **Then** the FK holds and the transaction commits atomically
121+
122+
### Entity schema-guard test lives in core and covers all entities
123+
124+
- **Given** the schema-convention guard test in `spring-services-core`
125+
- **When** it scans the classpath for `@Entity` under `com.openelements.spring.base`
126+
- **Then** it finds all seven entities and asserts each carries `@Table(schema = DbSchema.NAME)`; a new
127+
entity without the schema fails the build
128+
129+
### No optional module contributes an entity
130+
131+
- **Given** any optional feature module on the classpath
132+
- **When** the classpath is scanned for `@Entity` types contributed by that module
133+
- **Then** none are found (all entities live in core)
134+
135+
## Aggregate / starter integration (carried over from spec 013, relocated)
136+
137+
### Zero-config starter test runs against the full classpath
138+
139+
- **Given** the aggregate test in `spring-services-all` with a test app declaring its own `@Entity` +
140+
repository in a separate package, no `@Import`/`@EntityScan`/`@EnableJpaRepositories`
141+
- **When** the context starts with the full reactor on the classpath
142+
- **Then** the app's own and the library's entities/repositories resolve, and representative
143+
optional-feature beans (e.g. a Slack and an MCP bean) are present
144+
145+
## Consumer migration (breaking change)
146+
147+
### Old coordinate no longer compiles
148+
149+
- **Given** an existing consumer depending on `com.open-elements:spring-services` (the old jar)
150+
- **When** it builds against the new release
151+
- **Then** resolution yields the reactor parent pom (no classes) and the consumer's compilation fails —
152+
signalling the required migration
153+
154+
### Migration to all-module restores behavior
155+
156+
- **Given** that same consumer switched to `com.open-elements:spring-services-all`
157+
- **When** it builds and starts
158+
- **Then** all previously available features are present and behavior matches the pre-split release
159+
160+
### Explicit-wiring consumer wires only core features
161+
162+
- **Given** a consumer on `spring-services-core` using `@Import(FullSpringServiceConfig.class)`
163+
- **When** the context starts
164+
- **Then** only the core feature beans are wired; optional features require adding their module or
165+
importing their feature config explicitly

0 commit comments

Comments
 (0)