Skip to content

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.

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 (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
Clean Architecture ringGranit projectContains
DomainGranit.{Module}Aggregates, value objects, interfaces (ports), events
ApplicationGranit.{Module}Services, orchestrators, checkers, managers
InfrastructureGranit.{Module}.EntityFrameworkCoreDbContext, EF stores (adapters)
InfrastructureGranit.{Module}.{Provider}S3, Keycloak, SMTP adapters
PresentationGranit.{Module}.EndpointsMinimal API routes, request/response DTOs
  • 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

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

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.cs

The 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
CriterionClean ArchitectureVertical Slice Architecture
Code organizationBy technical layerBy feature / use case
Dependency directionAlways toward the center (domain)Within the slice
Where DDD livesDedicated domain layerShared Domain/ folder, used by all slices
Testing strategyMock boundaries between layersTest each slice end-to-end
Change radiusHorizontal — a field change touches all layersVertical — a feature change touches one folder
Abstraction levelHigh — interfaces for everythingLow — abstractions only when needed
Team scalingMultiple developers can own different layersMultiple developers can own different features
Granit supportNative — the framework’s own internal structureSupported — REPR, CQRS, modules, and DDD building blocks all work

Granit takes an explicit stance on framework internals but leaves the choice to application developers:

  1. 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.

  2. Application code built on Granit can adopt either style. The building blocks — AggregateRoot, SingleValueObject<T>, IReader/IWriter, MapGranitGroup(), IDomainEvent, FluentValidation — are architecture-style agnostic.

  3. 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.