Files
spring-modulith-demo/order-fulfillment/README.md
T

77 lines
4.0 KiB
Markdown

# order-fulfillment
Companion code for *[Spring Modulith 2.1: Enforcing Module Boundaries Inside a Spring Boot Monolith](https://ankurm.com)* on [ankurm.com](https://ankurm.com).
A 4-module Spring Boot monolith — `order`, `inventory`, `shipping`, `notification` — verified
with Spring Modulith's `ApplicationModules.verify()`. Everything in the post was produced by
actually running this code; see `output/` for every captured transcript, in order.
## Versions
| Artifact | Version |
|---|---|
| Spring Boot | 4.1.1 |
| Spring Modulith | 2.1.1 (current GA; 2.2 exists only as milestones as of this writing — verified against Maven Central's `maven-metadata.xml`, not the project's own `<release>` tag, which has pointed at a milestone before) |
| JDK | 25 (LTS) |
| H2 | 2.4.240 (in-memory, for the demo only) |
## Quickstart
```
mvn -pl order-fulfillment -am test
```
Runs the module-boundary verification, the documentation generator, and the full
`order → inventory → shipping → notification` event-cascade integration test.
To run the app itself:
```
mvn -pl order-fulfillment -am spring-boot:run
```
```
curl -X POST "http://localhost:8080/orders?sku=WIDGET-1&quantity=3"
curl "http://localhost:8080/orders/1"
```
## Modules
| Module | Public API | Internal | Listens for |
|---|---|---|---|
| `order` | `OrderManagement`, `OrderController` | `internal.Order`, `internal.OrderRepository` | — |
| `inventory` | `InventoryManagement` | `internal.Stock`, `internal.StockRepository`, `internal.StockSeeder` | `OrderPlaced` |
| `shipping` | `ShippingManagement` | `internal.Shipment`, `internal.ShipmentRepository` | `StockReserved` |
| `notification` | `NotificationManagement` | `internal.Notification`, `internal.NotificationRepository` | `OrderPlaced`, `ShipmentScheduled` |
`order` has zero module dependencies — on purpose. See the Javadoc on `OrderManagement`
for why, and `output/03-cycle-via-public-api-still-failed.txt` for what happens the first
time that seems like a good idea to "fix" by adding one back.
## Endpoints
| Method | Path | Description |
|---|---|---|
| `POST` | `/orders?sku={sku}&quantity={n}` | Place an order |
| `GET` | `/orders/{id}` | Fetch an order |
## Captured output (`output/`)
Every number and every stack trace in the post traces to one of these, byte for byte:
| File | What it captures |
|---|---|
| `00-table-name-collision.txt` | H2 rejecting `CREATE TABLE order` — `ORDER` is a reserved word |
| `01-verify-violation-failed.txt` | `ApplicationModules.verify()` failing on a direct reference to `inventory.internal.StockRepository` — both a cycle and a non-exposed-type violation |
| `02-verify-passed.txt` | The same test, green, after removing the dependency on `inventory` entirely |
| `03-cycle-via-public-api-still-failed.txt` | Why routing the same call through `InventoryManagement` (the real public API) still fails — a cycle isn't a visibility problem |
| `04-event-flow-missing-dependency.txt` | `@ApplicationModuleTest(mode = ALL_DEPENDENCIES)` bootstrapping *zero* other modules for `order`, because `order` has no outgoing dependencies, and the resulting event-cascade test timing out |
| `05-redundant-transactional-failure.txt` | Stacking a plain `@Transactional` on top of `@ApplicationModuleListener` failing bean registration outright |
| `06-event-flow-passed.txt` | The full `order → inventory → shipping → notification` cascade, fixed, with both notification hops firing |
| `07-generated-docs.txt` | The real output of `Documenter.writeModulesAsPlantUml().writeModuleCanvases()...` — the PlantUML component diagram and per-module canvases this repo's own structure produces |
| `08-javap-listener-meta-annotations.txt` | `javap -v` on the compiled `@ApplicationModuleListener` class, showing its real meta-annotations (`@Async` + `@Transactional(REQUIRES_NEW)` + `@TransactionalEventListener`) |
No `docs/` chapter directory in this repository — the intermediate and reference-depth
material that would normally live there is in accordion sections inside the WordPress
post itself.