Architecture Styles — DDD, Clean Architecture & Vertical Slices
Domain-Driven Design (DDD) is a domain modeling discipline — aggregates, value objects, domain events, bounded contexts. It answers the question “how do I model the business domain?” but says nothing about how to organize application code around that model.
That is the job of the architecture style: Clean Architecture and Vertical Slice Architecture are two proven approaches. Granit supports both without structural changes. The choice depends on your team, your domain complexity, and your delivery cadence.
The orthogonality principle
Section titled “The orthogonality principle”DDD and architecture style operate on different axes. Picking one does not constrain the other.
quadrantChart
title DDD × Architecture Style
x-axis "Layer-First (Clean Architecture)" --> "Feature-First (Vertical Slices)"
y-axis "Anemic Domain" --> "Rich Domain (DDD)"
quadrant-1 "DDD + Vertical Slices"
quadrant-2 "DDD + Clean Architecture"
quadrant-3 "No DDD + Clean Architecture"
quadrant-4 "No DDD + Vertical Slices"
"Granit framework internals": [0.25, 0.75]
"Application code (your choice)": [0.60, 0.70]
- DDD defines tactical patterns:
AggregateRoot,SingleValueObject<T>,IDomainEvent, factory methods, invariant enforcement - Architecture style defines code organization: by layer (Clean Architecture) or by feature (Vertical Slice Architecture)
Both axes are independent. You can use DDD with either style, and you can use either style without DDD.
Clean Architecture
Section titled “Clean Architecture”Clean Architecture (Robert C. Martin) organizes code in concentric rings with a strict dependency rule: dependencies always point inward. The domain is at the center; infrastructure and presentation are at the edges.
flowchart TB
subgraph Presentation["Presentation Layer"]
EP["*.Endpoints<br/>Minimal API routes"]
end
subgraph Application["Application Layer"]
SVC["Services, Orchestrators<br/>Use cases, business workflows"]
end
subgraph Domain["Domain Layer"]
AGG["AggregateRoot, Entity<br/>Value Objects, Domain Events"]
PORT["IReader, IWriter<br/>Ports (interfaces)"]
end
subgraph Infrastructure["Infrastructure Layer"]
EF["*.EntityFrameworkCore<br/>DbContext, EF stores"]
EXT["*.S3, *.Keycloak<br/>External providers"]
end
EP --> SVC
SVC --> AGG
SVC --> PORT
EF -->|implements| PORT
EXT -->|implements| PORT
style Domain fill:#2d5a27,color:#fff
style Application fill:#4a9eff,color:#fff
style Presentation fill:#7c4dff,color:#fff
style Infrastructure fill:#ff6b6b,color:#fff
Mapping to Granit projects
Section titled “Mapping to Granit projects”| Clean Architecture ring | Granit project | Contains |
|---|---|---|
| Domain | Granit.{Module} | Aggregates, value objects, interfaces (ports), events |
| Application | Granit.{Module} | Services, orchestrators, checkers, managers |
| Infrastructure | Granit.{Module}.EntityFrameworkCore | DbContext, EF stores (adapters) |
| Infrastructure | Granit.{Module}.{Provider} | S3, Keycloak, SMTP adapters |
| Presentation | Granit.{Module}.Endpoints | Minimal API routes, request/response DTOs |
When to choose Clean Architecture
Section titled “When to choose Clean Architecture”- Large teams (5+ developers) working across the same domain
- Complex domains with many cross-cutting business rules
- Long-lived projects where testability and maintainability are critical
- Regulatory environments requiring strict separation of concerns (GDPR, ISO 27001)
- When the domain model is shared across many use cases
- Premature abstraction: do not create interfaces “just in case”
- Layer ceremony: simple CRUD operations still pass through all layers
- Horizontal changes: modifying a data field requires touching domain, application, infrastructure, and presentation
- Over-injection: service constructors with 8+ dependencies signal a bloated application layer
Vertical Slice Architecture
Section titled “Vertical Slice Architecture”Vertical Slice Architecture (Jimmy Bogard) organizes code by feature. Each slice contains everything needed to handle a single use case: request, validation, handler, persistence, and response. Slices share infrastructure but not business logic.
flowchart LR
subgraph S1["CreatePatient"]
direction TB
S1R["Request"] --> S1V["Validator"] --> S1H["Handler"] --> S1Res["Response"]
end
subgraph S2["TransferPatient"]
direction TB
S2R["Request"] --> S2V["Validator"] --> S2H["Handler"] --> S2Res["Response"]
end
subgraph S3["DischargePatient"]
direction TB
S3R["Request"] --> S3V["Validator"] --> S3H["Handler"] --> S3Res["Response"]
end
DOMAIN["Shared Domain<br/>(Aggregates, Value Objects)"]
S1H --> DOMAIN
S2H --> DOMAIN
S3H --> DOMAIN
style S1 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
style S2 fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
style S3 fill:#fff3e0,stroke:#ef6c00,color:#e65100
style DOMAIN fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c
Mapping to Granit
Section titled “Mapping to Granit”Each Granit module is already a coarse-grained vertical slice (isolated DbContext, own endpoints, own domain). Application developers building on Granit can go further and organize their own modules as fine-grained vertical slices.
src/App.PatientManagement/├── Domain/│ ├── Patient.cs # Shared aggregate root│ └── ValueObjects/│ └── MedicalRecordNumber.cs├── Features/│ ├── CreatePatient/│ │ ├── CreatePatientRequest.cs│ │ ├── CreatePatientValidator.cs│ │ ├── CreatePatientHandler.cs│ │ └── CreatePatientResponse.cs│ ├── TransferPatient/│ │ └── ...│ └── DischargePatient/│ └── ...├── Persistence/│ └── PatientManagementDbContext.cs└── PatientManagementModule.csThe REPR pattern already groups endpoints by feature, and the Vertical Slice Architecture pattern page documents the full approach.
When to choose Vertical Slice Architecture
Section titled “When to choose Vertical Slice Architecture”- Small teams (1—4 developers) shipping features independently
- Rapid delivery cadence where changes must be localized
- Features with distinct data needs and limited cross-cutting rules
- Greenfield applications where you want to avoid premature abstraction
- When each feature has a different level of complexity
- Code duplication across slices if shared logic is not extracted into the domain
- Discovering cross-cutting invariants late — they still need a shared domain model
- Loss of global visibility: harder to see “all the things that happen when a patient is admitted” when logic is spread across slices
- Without discipline, slices can become mini-monoliths with duplicated persistence logic
Comparison
Section titled “Comparison”| Criterion | Clean Architecture | Vertical Slice Architecture |
|---|---|---|
| Code organization | By technical layer | By feature / use case |
| Dependency direction | Always toward the center (domain) | Within the slice |
| Where DDD lives | Dedicated domain layer | Shared Domain/ folder, used by all slices |
| Testing strategy | Mock boundaries between layers | Test each slice end-to-end |
| Change radius | Horizontal — a field change touches all layers | Vertical — a feature change touches one folder |
| Abstraction level | High — interfaces for everything | Low — abstractions only when needed |
| Team scaling | Multiple developers can own different layers | Multiple developers can own different features |
| Granit support | Native — the framework’s own internal structure | Supported — REPR, CQRS, modules, and DDD building blocks all work |
Granit’s position
Section titled “Granit’s position”Granit takes an explicit stance on framework internals but leaves the choice to application developers:
-
Framework modules follow Clean Architecture — hexagonal + layered structure with ports, adapters, and strict dependency rules. This is a deliberate choice for a framework consumed by many applications.
-
Application code built on Granit can adopt either style. The building blocks —
AggregateRoot,SingleValueObject<T>,IReader/IWriter,MapGranitGroup(),IDomainEvent,FluentValidation— are architecture-style agnostic. -
Hybrid approaches are valid. Many teams start with Clean Architecture for core domain modules and use VSA for simpler CRUD features. Granit’s module system supports this: each module can follow its own internal organization.
Further reading
Section titled “Further reading”Granit patterns
Section titled “Granit patterns”- Hexagonal Architecture — Ports & Adapters
- Layered Architecture — Module Layer Split
- Vertical Slice Architecture — Feature-Organized Code
- REPR — Request-Endpoint-Response
- CQRS — Reader/Writer Separation
- DDD Aggregate Roots
- ADR-017 — DDD Aggregate Root & Value Object Strategy
- Modular Monolith vs Microservices