{"id":49566362,"url":"https://github.com/ferderer/recursive-modulith","last_synced_at":"2026-05-03T11:48:14.963Z","repository":{"id":339641832,"uuid":"1162799446","full_name":"ferderer/recursive-modulith","owner":"ferderer","description":"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.","archived":false,"fork":false,"pushed_at":"2026-03-27T14:54:18.000Z","size":49,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T11:48:12.295Z","etag":null,"topics":["arc42","architecture","architecture-decision-records","best-practices","bounded-context","ddd","java","modulith","project-structure","spring-boot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ferderer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-20T17:56:06.000Z","updated_at":"2026-03-27T14:54:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ferderer/recursive-modulith","commit_stats":null,"previous_names":["ferderer/recursive-modulith"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ferderer/recursive-modulith","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ferderer%2Frecursive-modulith","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ferderer%2Frecursive-modulith/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ferderer%2Frecursive-modulith/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ferderer%2Frecursive-modulith/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ferderer","download_url":"https://codeload.github.com/ferderer/recursive-modulith/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ferderer%2Frecursive-modulith/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32568036,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arc42","architecture","architecture-decision-records","best-practices","bounded-context","ddd","java","modulith","project-structure","spring-boot"],"created_at":"2026-05-03T11:48:14.320Z","updated_at":"2026-05-03T11:48:14.956Z","avatar_url":"https://github.com/ferderer.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Recursive-Modulith\n*– Architecture is perfect when nothing can be taken away.*\n\n**A pragmatic, recursive package structure for Spring Boot modular monoliths.**\n\nAlso known as **Matryoshka Architecture** 🪆 — because every level contains the same pattern, just smaller.\n\n---\n\n## The Problem\n\nSpring Boot has no official guidance for package structures beyond tutorials. Existing approaches each solve part of the puzzle:\n\n| Approach | Strength | Weakness |\n|---|---|---|\n| Package-by-Layer | Easy to start | No cohesion, no encapsulation |\n| Package-by-Feature | High cohesion | No answer for cross-cutting concerns |\n| Hexagonal / Clean | Strong boundaries | Massive boilerplate, over-engineered for most projects |\n| Spring Modulith | Module verification | No guidance for internal structure |\n\n**None of them address all levels consistently. That's the gap this project fills.**\n\n## The Solution\n\nOne recursive pattern, applied at every level:\n\n```\n{level}/\n├── config/        ← framework setup (top-level only)\n├── common/        ← shared code (any level)\n└── {domain}/      ← bounded context, use case, sub-module\n```\n\nApp → Bounded Context → Use Case → Action — same structure, all the way down.\n\n## Quick Reference\n\n```\nconfig/       → Framework setup. Top-level only. Never import from domain code.\ncommon/       → Shared code. Any level. Visible downward.\n{bc}/         → Bounded context. Public API = Service + Events.\n  common/     → BC-internal shared code. domain/, error/, persistence/.\n  {usecase}/  → One endpoint = one class. No service layer.\n    Class     → Action name. Request/Response as inner records.\n    Entity    → JPA. Postfix \"Entity\". Domain class without postfix.\n```\n\n## Full Example\n\n```\ncom.acme.insuranceapp\n├── Application.java\n│\n├── config/\n│   ├── security/\n│   │   ├── SecurityConfig.java\n│   │   └── JwtTokenService.java\n│   ├── web/\n│   │   ├── CorsConfig.java\n│   │   └── JacksonConfig.java\n│   ├── error/\n│   │   └── GlobalExceptionHandler.java\n│   ├── persistence/\n│   │   └── AuditingConfig.java\n│   └── openapi/\n│       └── OpenApiConfig.java\n│\n├── common/\n│   ├── Tsid.java\n│   ├── domain/\n│   │   ├── Money.java\n│   │   ├── Address.java\n│   │   └── Currency.java\n│   ├── error/\n│   │   └── AppError.java\n│   └── persistence/\n│       └── BaseEntity.java\n│\n├── policy/                                ← Bounded Context\n│   ├── PolicyService.java                  ← Public API (facade only)\n│   ├── PolicyActivatedEvent.java           ← Public Event\n│   ├── common/\n│   │   ├── domain/\n│   │   │   ├── PolicyDraft.java            ← Domain class (clean name)\n│   │   │   ├── PolicyDraftEntity.java      ← JPA entity (postfix)\n│   │   │   └── PolicyStatus.java\n│   │   ├── error/\n│   │   │   └── PolicyError.java            ← Guard4j error enum\n│   │   └── persistence/\n│   │       └── PolicyDraftRepository.java  ← shared by ≥2 use cases\n│   ├── creation/\n│   │   ├── CreatePolicyDraft.java          ← POST endpoint\n│   │   ├── GetPolicyDraft.java             ← GET endpoint\n│   │   └── submitpolicydraft/              ← escalated (complex)\n│   │       ├── SubmitPolicyDraft.java\n│   │       ├── SubmitValidator.java\n│   │       └── UnderwritingResult.java\n│   └── renewal/\n│       └── RenewPolicy.java\n│\n├── claims/                                ← Bounded Context\n│   ├── ClaimsService.java\n│   ├── common/\n│   │   ├── error/\n│   │   │   └── ClaimsError.java\n│   │   └── persistence/\n│   │       └── ClaimRepository.java\n│   ├── filing/\n│   │   ├── FileClaim.java\n│   │   └── GetClaim.java\n│   └── policycancelled/\n│       └── HandlePolicyCancelled.java     ← Event listener = use case\n│\n└── billing/\n    ├── BillingService.java\n    ├── common/\n    │   └── error/\n    │       └── BillingError.java\n    ├── invoice/\n    └── payment/\n```\n\n## What a Use Case Looks Like\n\nOne endpoint, one class, no service layer:\n\n```java\n@RestController\n@RequestMapping(\"/api/v1/policies/drafts\")\n@Transactional\nclass CreatePolicyDraft {\n\n    record Request(String holderName, Coverage coverage) {}\n    record Response(UUID id, String holderName, Status status) {}\n\n    private final PolicyDraftRepository repo;\n    private final TsidGenerator tsid;\n\n    @PostMapping\n    Response handle(@RequestBody Request req) {\n        var draft = PolicyDraft.create(tsid.next(), req.holderName(), req.coverage());\n        repo.save(draft);\n        return new Response(draft.id(), draft.holderName(), draft.status());\n    }\n}\n```\n\nExtract a service **only when** a second caller appears.\n\n## Key Rules\n\n### Dependency Rules\n\n| Rule | Enforcement |\n|---|---|\n| Domain code must not import `config.*` | ArchUnit |\n| BC-to-BC access only via `{Bc}Service` or Events | Modulith verify() |\n| No direct use-case-to-use-case references | ArchUnit |\n| `@Transactional` only on use-case classes | ArchUnit |\n\n### Naming Conventions\n\n| Postfix | When | Example |\n|---|---|---|\n| *(none)* | Domain class, DTO, value object | `PolicyDraft`, `Money` |\n| *(none)* | Endpoint (action name) | `CreatePolicyDraft` |\n| `Entity` | JPA class | `PolicyDraftEntity` |\n| `Repository` | Spring Data | `PolicyDraftRepository` |\n| `Service` | BC public API (facade) | `PolicyService` |\n| `Error` | Guard4j error enum | `PolicyError` |\n\nNo `Controller` postfix. No `Dto` postfix.\n\n### When to Escalate\n\n| Signal | Threshold | Action |\n|---|---|---|\n| Classes in use-case package | Mapper/Validator or ≥3 | Sub-package for endpoint |\n| Use cases per BC | \u003e25–30 | Resource grouping |\n| Classes per BC | \u003e60–80 | Consider sub-BC |\n| Aggregates per BC | \u003e12–15 | Consider sub-BC |\n| ArchUnit cycles | Any | Resolve immediately |\n\n### Error Handling (3 Layers)\n\n```\ncommon/error/AppError.java              ← App-wide errors (Guard4j enum)\n{bc}/common/error/{Bc}Error.java        ← BC-specific errors (Guard4j enum)\nconfig/error/GlobalExceptionHandler.java ← Exception → ProblemDetail mapping\n```\n\n### CI Verification\n\n```java\n@Test\nvoid verifyModulithStructure() {\n    ApplicationModules.of(Application.class).verify();\n}\n```\n\n## Documentation\n\n| Document | Purpose |\n|---|---|\n| [Architecture Decision Records](docs/adrs/) | All 23 ADRs with context, decision, rationale |\n| [arc42 Documentation](docs/arc42.md) | Full architecture documentation |\n| [Ruleset](docs/regelwerk.md) | Practical reference (German) |\n| [Analysis](docs/analyse.md) | Comparison of existing approaches |\n\n## Why \"Matryoshka\"?\n\nLike Russian nesting dolls:\n\n- 🪆 Every doll has the **same shape** → every level follows `config/` + `common/` + `{domain}/`\n- 🪆 Dolls are **nested inside each other** → App → [Domain] → [Subdomain] → Bounded Context → Use Case → Action\n- 🪆 Each doll is **self-contained** → every BC is extractable to a microservice\n- 🪆 From outside, you only see the **outer shell** → public API\n\n## Status\n\n- [x] Architecture analysis \u0026 comparison\n- [x] Architecture Decision Records (ADR-001 to ADR-023)\n- [x] arc42 documentation\n- [x] Practical ruleset\n- [ ] Reference implementation\n- [ ] Custom Spring Initializer (generator)\n- [ ] Article series\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fferderer%2Frecursive-modulith","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fferderer%2Frecursive-modulith","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fferderer%2Frecursive-modulith/lists"}