Skip to content

Latest commit

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Recursive-Modulith

– Architecture is perfect when nothing can be taken away.

A pragmatic, recursive package structure for Spring Boot modular monoliths.

Also known as Matryoshka Architecture 🪆 — because every level contains the same pattern, just smaller.


The Problem

Spring Boot has no official guidance for package structures beyond tutorials. Existing approaches each solve part of the puzzle:

ApproachStrengthWeakness
Package-by-LayerEasy to startNo cohesion, no encapsulation
Package-by-FeatureHigh cohesionNo answer for cross-cutting concerns
Hexagonal / CleanStrong boundariesMassive boilerplate, over-engineered for most projects
Spring ModulithModule verificationNo guidance for internal structure

None of them address all levels consistently. That's the gap this project fills.

The Solution

One recursive pattern, applied at every level:

{level}/
├── config/ ← framework setup (top-level only)
├── common/ ← shared code (any level)
└── {domain}/ ← bounded context, use case, sub-module

App → Bounded Context → Use Case → Action — same structure, all the way down.

Quick Reference

config/ → Framework setup. Top-level only. Never import from domain code.
common/ → Shared code. Any level. Visible downward.
{bc}/ → Bounded context. Public API = Service + Events.
common/ → BC-internal shared code. domain/, error/, persistence/.
{usecase}/ → One endpoint = one class. No service layer.
Class → Action name. Request/Response as inner records.
Entity → JPA. Postfix "Entity". Domain class without postfix.

Full Example

com.acme.insuranceapp
├── Application.java
│
├── config/
│ ├── security/
│ │ ├── SecurityConfig.java
│ │ └── JwtTokenService.java
│ ├── web/
│ │ ├── CorsConfig.java
│ │ └── JacksonConfig.java
│ ├── error/
│ │ └── GlobalExceptionHandler.java
│ ├── persistence/
│ │ └── AuditingConfig.java
│ └── openapi/
│ └── OpenApiConfig.java
│
├── common/
│ ├── Tsid.java
│ ├── domain/
│ │ ├── Money.java
│ │ ├── Address.java
│ │ └── Currency.java
│ ├── error/
│ │ └── AppError.java
│ └── persistence/
│ └── BaseEntity.java
│
├── policy/ ← Bounded Context
│ ├── PolicyService.java ← Public API (facade only)
│ ├── PolicyActivatedEvent.java ← Public Event
│ ├── common/
│ │ ├── domain/
│ │ │ ├── PolicyDraft.java ← Domain class (clean name)
│ │ │ ├── PolicyDraftEntity.java ← JPA entity (postfix)
│ │ │ └── PolicyStatus.java
│ │ ├── error/
│ │ │ └── PolicyError.java ← Guard4j error enum
│ │ └── persistence/
│ │ └── PolicyDraftRepository.java ← shared by ≥2 use cases
│ ├── creation/
│ │ ├── CreatePolicyDraft.java ← POST endpoint
│ │ ├── GetPolicyDraft.java ← GET endpoint
│ │ └── submitpolicydraft/ ← escalated (complex)
│ │ ├── SubmitPolicyDraft.java
│ │ ├── SubmitValidator.java
│ │ └── UnderwritingResult.java
│ └── renewal/
│ └── RenewPolicy.java
│
├── claims/ ← Bounded Context
│ ├── ClaimsService.java
│ ├── common/
│ │ ├── error/
│ │ │ └── ClaimsError.java
│ │ └── persistence/
│ │ └── ClaimRepository.java
│ ├── filing/
│ │ ├── FileClaim.java
│ │ └── GetClaim.java
│ └── policycancelled/
│ └── HandlePolicyCancelled.java ← Event listener = use case
│
└── billing/
├── BillingService.java
├── common/
│ └── error/
│ └── BillingError.java
├── invoice/
└── payment/

What a Use Case Looks Like

One endpoint, one class, no service layer:

@RestController@RequestMapping("/api/v1/policies/drafts")
@TransactionalclassCreatePolicyDraft {
recordRequest(StringholderName, Coveragecoverage) {}
recordResponse(UUIDid, StringholderName, Statusstatus) {}
privatefinalPolicyDraftRepositoryrepo;
privatefinalTsidGeneratortsid;
@PostMappingResponsehandle(@RequestBodyRequestreq) {
vardraft = PolicyDraft.create(tsid.next(), req.holderName(), req.coverage());
repo.save(draft);
returnnewResponse(draft.id(), draft.holderName(), draft.status());
}
}

Extract a service only when a second caller appears.

Key Rules

Dependency Rules

RuleEnforcement
Domain code must not import config.*ArchUnit
BC-to-BC access only via {Bc}Service or EventsModulith verify()
No direct use-case-to-use-case referencesArchUnit
@Transactional only on use-case classesArchUnit

Naming Conventions

PostfixWhenExample
(none)Domain class, DTO, value objectPolicyDraft, Money
(none)Endpoint (action name)CreatePolicyDraft
EntityJPA classPolicyDraftEntity
RepositorySpring DataPolicyDraftRepository
ServiceBC public API (facade)PolicyService
ErrorGuard4j error enumPolicyError

No Controller postfix. No Dto postfix.

When to Escalate

SignalThresholdAction
Classes in use-case packageMapper/Validator or ≥3Sub-package for endpoint
Use cases per BC>25–30Resource grouping
Classes per BC>60–80Consider sub-BC
Aggregates per BC>12–15Consider sub-BC
ArchUnit cyclesAnyResolve immediately

Error Handling (3 Layers)

common/error/AppError.java ← App-wide errors (Guard4j enum)
{bc}/common/error/{Bc}Error.java ← BC-specific errors (Guard4j enum)
config/error/GlobalExceptionHandler.java ← Exception → ProblemDetail mapping

CI Verification

@TestvoidverifyModulithStructure() {
ApplicationModules.of(Application.class).verify();
}

Documentation

DocumentPurpose
Architecture Decision RecordsAll 23 ADRs with context, decision, rationale
arc42 DocumentationFull architecture documentation
RulesetPractical reference (German)
AnalysisComparison of existing approaches

Why "Matryoshka"?

Like Russian nesting dolls:

  • 🪆 Every doll has the same shape → every level follows config/ + common/ + {domain}/
  • 🪆 Dolls are nested inside each other → App → [Domain] → [Subdomain] → Bounded Context → Use Case → Action
  • 🪆 Each doll is self-contained → every BC is extractable to a microservice
  • 🪆 From outside, you only see the outer shell → public API

Status

  • Architecture analysis & comparison
  • Architecture Decision Records (ADR-001 to ADR-023)
  • arc42 documentation
  • Practical ruleset
  • Reference implementation
  • Custom Spring Initializer (generator)
  • Article series

License

MIT

About

Recursive-Modulith (Matryoshka Architecture) 🪆 — A pragmatic, recursive package structure for Spring Boot. Same pattern at every level: config/ + common/ + domain. With ADRs, arc42 docs, and CI-verifiable rules via Spring Modulith + ArchUnit.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages