architecture-composition
How to combine architecture styles correctly — Clean/Hexagonal/Onion, Vertical Slices, DDD tactical patterns, CQRS, Modular Monolith, transaction scripts — per module, using axes, a compatibility matr
SKILL.md.claude/skills/architecture-composition/SKILL.md
GitHubArchitecture Composition
1. The model: styles answer different questions
| Axis | Question it answers | Values | Scale |
|---|---|---|---|
| A1 Topology | How many deployables? | monolith · modular-monolith · microservices | system |
| A2 Boundaries | Where are the business seams? | modules / bounded contexts | system |
| A3 Dependency direction | Which code may depend on which? | clean · hexagonal · onion · layered · none | module |
| A4 Organization | How are files grouped? | vertical-slices · technical-layers | module |
| A5 Request model | Same model for reads and writes? | none · separate-methods · separate-models · separate-stores | module |
| A6 Domain logic | Where do business rules live? | transaction-script · table-module · domain-model | module |
| A7 Integration | How do modules interact? | contracts (in-proc) · integration events (+outbox) · messaging | between modules |
| A8 Persistence | How is state accessed? | ef-core · dapper · stored-procedures · (event-sourcing) | module / use case |
Composition law. Exactly one value per axis per module. Styles on different axes compose. Two styles on the same axis in the same module conflict. Different modules may choose differently — that is the purpose of A2.
Nesting. A1 contains A2; each A2 module decides A3–A6 and A8; A7 connects modules. A decision at a larger scale constrains smaller ones (microservices forbid shared-database A8; domain-model A6 forbids invariants in SQL), never the reverse.
2. Recipes (named, valid compositions)
| Recipe | A3 | A4 | A6 | Typical A5 | Use when |
|---|---|---|---|---|---|
| clean-sliced | clean | vertical-slices | domain-model | separate-models | core subdomain, rich invariants, long life, several adapters |
| sliced-domain | none (rules via tests) | vertical-slices | domain-model | separate-methods | real invariants but small module; a Domain project would be ceremony |
| pure-slices | none | vertical-slices | transaction-script / table-module | separate-methods | CRUD-ish, supporting/generic subdomain, reporting |
| hexagonal-integration | hexagonal | vertical-slices | transaction-script | separate-methods | module dominated by external systems; needs anti-corruption layer |
| layered-legacy | layered | technical-layers | any | none | existing code only; frozen, migrated via architecture-migration |
Details, folder layouts and "where does X go" for each recipe: references/placement-rules.md.
3. Choosing a recipe for a module (fast path)
Score the module; each yes = 1 point.
- Are there rules that span several entities and must hold after every change (invariants)?
- Does an entity have a lifecycle/state machine with guarded transitions?
- Are the rules regulated, audited, or expensive when wrong (fiscal, medical, financial)?
- Will the rules change often and independently of the UI/DB?
- Are there ≥ 2 real adapters for the same capability (API + jobs + messaging, or 2 providers)?
- Will the module live > 3 years with more than one developer?
- 0–1 → pure-slices
- 2–3 → sliced-domain (or hexagonal-integration if Q5 is the main driver and rules are thin)
- 4–6 → clean-sliced
Then choose A5: separate-models if read shapes differ materially from the write model or reads
dominate; separate-stores only with measured read-scale or reporting isolation needs; otherwise
separate-methods. Full decision framework: skill architecture-selection.
4. Compatibility matrix (summary)
Full matrix with reasoning: references/compatibility-matrix.md.
| Combination (same module) | Verdict | Condition / reason |
|---|---|---|
| Clean + Vertical Slices | ✅ | Clean sets project dependencies; slices organize Application (and optionally Api) |
| Hexagonal + Vertical Slices | ✅ | slice = driving adapter + use case; ports only for driven dependencies |
| Domain Model + Vertical Slices | ✅ | aggregates shared across slices; slices never share handlers |
| Clean + Transaction Script | ⚠️ | legal but usually ceremony: empty Domain layer. Prefer pure-slices |
| Technical layers + Vertical Slices | ❌ | same axis A4. Pick one per module |
| Domain Model + business rules in SPs | ❌ | invariants split across two places; SPs read-side only |
| CQRS separate-stores + Transaction Script | ⚠️ | complexity with no domain to protect; justify with measured read load |
| Mediator everywhere + direct handlers | ❌ | same concern (dispatch), pick one convention per system |
| Microservices + shared database writes | ❌ | A1 forbids it; that is a distributed monolith |
| Modular Monolith + different recipe per module | ✅ | intended; enforce boundaries with tests |
| Event Sourcing + Transaction Script | ❌ | event sourcing needs an aggregate to decide events |
5. Rules that make combinations work
- Boundary before structure. Decide A2 before A3/A4. A perfect Clean module with the wrong boundary is a well-organized mistake.
- Share down, not sideways. Slices share the domain model and infrastructure, never each other. If slice B needs slice A's logic, move that logic into the domain (aggregate/domain service) or a module-internal service — never call A's handler.
- Rule of three for shared application code. Duplicate across two slices; extract on the third, into the lowest layer that makes sense.
- Ports only where there is a real seam: external system, second implementation, or a test seam
that cannot use the real thing.
IRepositoryover EF Core in a pure-slices module is not a seam. - Contracts are the module's only public surface. Everything else is
internal. - Every structural rule is either an architecture test or explicitly
enforce: review.
6. Anti-patterns (flag as MAJOR unless noted)
- Ceremony Clean — 4 projects, repositories, mappers, mediator for a CRUD module.
- Anemic Domain project — entities with public setters, logic in handlers, inside
clean-sliced. - Slice-to-slice calls — handler injecting another slice's handler. (BLOCKER in clean-sliced)
- Common/Shared/Services dumping ground at module root.
- Generic repository over EF Core — hides the query capabilities you already have.
- Cross-module DB joins in write paths. (BLOCKER)
- Contracts leaking domain types — integration events carrying aggregates or EF entities. (BLOCKER)
- Recipe drift — new slices in a different style than the module's recipe.
7. Output when this skill is used for a decision
MODULE: <name> SUBDOMAIN: <core|supporting|generic> SCORE: <n/6>
RECIPE: <recipe> AXES: A3=<> A4=<> A5=<> A6=<> A8(write/read)=<>
WHY: <forces → choice, 2–4 lines>
REJECTED: <recipe — reason>
ENFORCEMENT: <architecture tests to add>
References
references/placement-rules.md— per-recipe folder layout and "where does X go" table. Read before placing any file.references/compatibility-matrix.md— full pairwise matrix with reasons and mitigations.references/profile-schema.md— every field ofprofile.ymland how agents use it.- Frontend:
references/frontend-composition.md— applying the same axes to React/TS (feature-sliced) and Blazor.