# Vertical slices

> Inside a module, code is grouped by use case, not by layer. Each slice folder holds its endpoint, command, handler and the C# 14 extension block that changes the aggregate.

Source page: https://appointme.dev/docs/concepts/vertical-slices/

* **Slice:** One folder per use case
* **Named:** Verb first: `RegisterCustomer/`
* **Domain logic:** An `extension(Customer)` block in the slice
* **Shared code:** The feature folder and its `Database/`

## How a feature folder is laid out

**A feature folder holds the aggregate and the code several slices share. Each use case gets its own subfolder, named after the action.**

This is CRM’s `Customers/` feature, trimmed:

* Customers/

  * Customer.cs # the aggregate: properties only

  * CustomerId.cs

  * CustomerDto.cs # read model for the query slices

  * CustomerPermissions.cs

  * CustomerRehydrationSource.cs # CRM’s contract implementation

  * Database/ # shared by several slices

    * CustomerTypeConfiguration.cs # EF mapping
    * CustomerFilters.cs # query-filter names
    * CustomerQueries.cs # EF: LoadAsync
    * CustomersRepository.cs # Dapper reads
    * CustomerRecord.cs

  * RegisterCustomer/

    * RegisterCustomer.cs # extension(Customer): Register
    * RegisterCustomerCommand.cs
    * RegisterCustomerCommandHandler.cs
    * RegisterCustomerEndpoint.cs # POST /api/v1/customers
    * RegisterCustomerRequest.cs
    * RegisterCustomerResponse.cs

  * UpdateCustomer/

    * …

  * DeleteCustomer/

    * …

  * GetCustomers/

    * …

  * GetCustomerById/

    * …

  * GetNewCustomerStats/

    * …

  * …

One request runs through one folder:

*Figure 1. Everything specific to registering a customer sits in RegisterCustomer/.*

1. **RegisterCustomerEndpoint** — maps the request with `ToCommand()`
2. **RegisterCustomerCommandHandler** — reached through `bus.InvokeAsync`
3. **Customer.Register** — the extension in `RegisterCustomer.cs`

| What                                                               | Where                               | Why there                                   |
| ------------------------------------------------------------------ | ----------------------------------- | ------------------------------------------- |
| Endpoint, request, command, handler, response                      | The slice                           | Only this use case needs them               |
| The domain operation (`Customer.Register`)                         | The slice, in a file named after it | The business rule changes with the use case |
| A helper only one slice uses (`NewCustomerStatsCalculator`)        | That slice, `GetNewCustomerStats/`  | One user, so it stays local                 |
| The aggregate and its ID (`Customer`, `CustomerId`)                | The feature folder                  | Every slice uses them                       |
| Read models and permissions (`CustomerDto`, `CustomerPermissions`) | The feature folder                  | Several slices use them                     |
| EF mapping, filter names, EF and Dapper queries                    | `Database/` in the feature folder   | Shared infrastructure                       |

Tests follow the same shape: `AppointMe.Crm.Tests/Customers/RegisterCustomer/RegisterCustomerTests.cs` tests the `RegisterCustomer/` slice.

## Naming: the verb comes first

**Folders and files start with the action, so a module’s folder list reads as the list of things it can do.**

| Kind of slice                                 | Verbs        | Examples                                                                                |
| --------------------------------------------- | ------------ | --------------------------------------------------------------------------------------- |
| Business intent on the module’s own aggregate | Domain verbs | `RegisterCustomer`, `ScheduleAppointment`, `RescheduleAppointment`, `CancelAppointment` |
| A projection synced from another module       | CRUD verbs   | `CreateAttendee`, `UpdateAttendee`, `DeleteAttendee` (Booking’s copy of CRM customers)  |
| A query                                       | `Get…`       | `GetCustomers`, `GetCustomerById`, `GetNewCustomerStats`                                |
| Sync and background work                      | Task verbs   | `ReconcileAttendees`, `SeedDemoCustomers`, `TopUpDemoCustomers`                         |

Files inside a slice take the slice name plus a role: `RegisterCustomerCommand`, `RegisterCustomerCommandHandler`, `RegisterCustomerEndpoint`, `RegisterCustomerRequest` and `RegisterCustomerResponse`. Queries use `…Query` and `…QueryHandler`. A slice only has the files it needs: `DeleteCustomer/` has no request, because the ID comes from the route.

The convention has a few exceptions. `GetCustomers/` holds `GetCustomerQuery` (singular), and some Organizations folders are named for a screen rather than an action (`Me/`, `Companies/Onboarding/`).

## Domain operations are extension blocks

**The aggregate is a plain record. The code that creates or changes it is a C# 14 extension block that lives in the slice that needs it.**

src/CRM/AppointMe.Crm/Customers/Customer.cs

```cs
public sealed record Customer : AggregateRoot
{
    public required CustomerId Id { get; init; }
    public required CompanyId CompanyId { get; init; }
    public required PersonName Name { get; set; }
    public required DateOfBirth? DateOfBirth { get; set; }
    public required Gender? Gender { get; set; }
    public required Email? Email { get; set; }
    public required DateTimeOffset RegistrationDate { get; init; }
    public required bool IsDeleted { get; set; }
}
```

A factory is a static member in an `extension(Customer)` block. It builds the aggregate and raises the event:

src/CRM/AppointMe.Crm/Customers/RegisterCustomer/RegisterCustomer.cs

```cs
extension(Customer)
{
    public static Customer Register(CompanyId companyId, PersonName name, DateOfBirth? dateOfBirth,
        Gender? gender, Email? email, DateTimeOffset registrationDate)
    {
        var customer = new Customer
        {
            Id = new CustomerId(NewId()),
            // …
            IsDeleted = false
        };


        customer.Raise(new CustomerRegisteredEvent(
            customer.Id.Value, companyId.Value,
            name.FirstName, name.LastName, dateOfBirth?.Value, email?.Value));


        return customer;
    }
}
```

A mutator is an instance member in an `extension(Customer customer)` block:

src/CRM/AppointMe.Crm/Customers/DeleteCustomer/DeleteCustomer.cs

```cs
extension(Customer customer)
{
    public void Delete()
    {
        customer.IsDeleted = true;
        customer.Raise(new CustomerDeletedEvent(customer.Id.Value, customer.CompanyId.Value));
    }
}
```

At the call site they read like members of `Customer`. The handler sits in the same namespace as the extension, so it needs no `using`:

src/CRM/AppointMe.Crm/Customers/DeleteCustomer/DeleteCustomerCommandHandler.cs

```cs
var customer = await dbContext.Customers.LoadAsync(new CustomerId(command.CustomerId), cancellationToken);


customer.Delete();
await dbContext.SaveChangesAsync(cancellationToken);
```

`LoadAsync` is itself an extension, on `IQueryable<Customer>`, in `Customers/Database/CustomerQueries.cs`, because several slices load a customer by ID.

## Where event handlers go

**An event handler lives in the slice whose operation it calls, not in a shared `Handlers/` folder.**

Booking keeps a projection of CRM customers. Its projection slices hold only the domain operation, since no HTTP endpoint creates an attendee. The handlers for CRM’s events sit with the synchronization code they call:

* Attendees/

  * Attendee.cs

  * AttendeeId.cs

  * CreateAttendee/

    * CreateAttendee.cs # extension(Attendee): Create

  * UpdateAttendee/

    * UpdateAttendee.cs

  * DeleteAttendee/

    * DeleteAttendee.cs

  * ReconcileAttendees/

    * AttendeeSynchronizer.cs # turns a CustomerSnapshot into an upsert
    * UpsertAttendee.cs # calls Attendee.Create or attendee.Update
    * CustomerRegisteredEventHandler.cs
    * CustomerUpdatedEventHandler.cs
    * CustomerDeletedEventHandler.cs
    * ReconcileAttendeesCommandHandler.cs # the daily full re-sync
    * …

  * Database/

    * …

`CustomerRegisteredEventHandler` and `CustomerUpdatedEventHandler` both call `AttendeeSynchronizer.Apply`, the same code the daily reconciliation runs, so all three live in `ReconcileAttendees/`. `CustomerDeletedEventHandler` calls `attendee.Delete()` from `DeleteAttendee/` but sits with the other sync handlers, which shows the rule is a convention, applied with judgment. In Identity, `EmployeeInvitedHandler` handles Organizations’ `EmployeeInvited` event from `Users/InviteUser/`.

## Add a slice

| Step                           | Where                                         | What                                                                                                       |
| ------------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| 1. Name it                     | `<Feature>/<Verb><Noun>/`                     | A domain verb for business intent, `Get…` for a query, a CRUD verb for a projection                        |
| 2. Write the operation         | `<Verb><Noun>.cs` in the slice                | `extension(Aggregate)` for a factory, `extension(Aggregate instance)` for a change. Raise the event there. |
| 3. Add the message and handler | `…Command.cs`, `…CommandHandler.cs`           | The handler checks the permission, calls the operation and saves                                           |
| 4. Expose it                   | `…Endpoint.cs`, `…Request.cs`                 | An `IEndpoint` that maps the request to the command and calls `bus.InvokeAsync`                            |
| 5. Test it                     | `AppointMe.<Module>.Tests/<Feature>/<Slice>/` | Tests mirror the slice folder                                                                              |
| 6. Share only when needed      | The feature folder or its `Database/`         | Move code up when a second slice needs it, not before                                                      |

The step-by-step version, with the code for each file, is [Add an endpoint](/docs/guides/add-an-endpoint/). To build a whole feature end to end, follow [Build your first feature](/docs/start/first-feature/).

## Gotchas

| Symptom                                           | Cause                                                                                    | Fix                                                                                                                        |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `CS1061` on `customer.Delete()` outside its slice | Each slice has its own namespace, and the extension isn’t imported                       | Add the slice’s `using`, as Booking’s `CustomerDeletedEventHandler` does with `AppointMe.Booking.Attendees.DeleteAttendee` |
| A change is saved but no event is raised          | A handler set a property directly (`customer.Name = …`) instead of calling the operation | Change state only through the slice’s operation. The setters are public, so code review has to catch this.                 |
| Two slices need the same SQL                      | Each query handler was written in isolation                                              | Put the query in the feature’s repository (`CustomersRepository`) and call it from both                                    |
| A helper is used by two slices                    | It started in one slice                                                                  | Move it to the feature folder or `Database/`                                                                               |

### Why slices instead of layers?

A layered layout puts a use case’s request in one folder, its handler in another and its data access in a third. A slice puts it in one folder, so a change, a review and a deletion stay in one place. Coupling between slices is explicit: when two slices need the same code, it moves up into the feature folder where everyone can see it.

The cost is some deliberate duplication. `RegisterCustomerRequest` and `RegisterCustomerCommand` have the same fields today, so the HTTP shape and the message can change independently later. There’s more on this trade-off in [vs Clean Architecture](/docs/compare/clean-architecture/).

### Why extension blocks instead of methods on the aggregate?

A `Customer` class with every operation as a method grows with each use case and becomes the file every change touches. With C# 14 extension blocks, `Customer.Register(...)` and `customer.Delete()` still read like members, but each one lives in its slice, and deleting the slice deletes the operation.

The cost: an extension can only use the aggregate’s accessible members, so every property an operation changes has a public setter. Keeping state changes inside the operations is a convention, not something the compiler enforces.

## Next

* [Add an endpoint](/docs/guides/add-an-endpoint/) (Guide)
* [CQRS: EF Core writes, Dapper reads](/docs/concepts/cqrs/) (Concept)
* [Modular monolith](/docs/concepts/modular-monolith/) (Concept)
