Backend / Architecture & design / 24_vertical_slice_architecture.md

Vertical slice architecture

Updated 4 interview angles 4 min read source
On this page8
  1. The shape
  2. Why it exists
  3. The trade
  4. In Python
  5. When to choose which
  6. Where it breaks in production
  7. Related
  8. Interview angle

Vertical slice architecture

Layered, clean and hexagonal all organise code by kind: every controller together, every service together, every repository together. Vertical slice organises it by feature: everything one use case needs, in one place. It is the current counterweight to the domain-in-the-centre architectures, and interviewers ask about it because most teams have felt the pain it responds to.

The shape

text
layered                       vertical slice
  routers/                      features/
    orders.py                     create_order/
    refunds.py                      handler.py
  services/                       request.py
    orders.py                       response.py
    refunds.py                      test_create_order.py
  repositories/                   refund_order/
    orders.py                       handler.py
    refunds.py                      ...

To change how an order is created in the layered layout you touch three folders, and each file also contains the other features’ code. In the slice layout you open one folder, and nothing in it belongs to another feature.

The rule is that slices do not import each other. Shared code moves down into a small shared kernel — the domain model, infrastructure adapters — and that kernel stays small on purpose. The moment slices reach sideways into each other, you have a layered architecture with a confusing folder structure.

Why it exists

Two failures of layered code drive it:

  • Every change is a cross-cutting change. Adding a field to an order means editing the router, the service, the repository and the schema — four files, four folders, and a review that has to hold all of them in view.
  • Layers accumulate the wrong abstractions. A services/orders.py grows to a thousand lines because every order-related use case lands in it, and a method that only one endpoint calls gets treated as a shared interface.

The slice answer is that the unit of cohesion is the use case, not the technical role. Code that changes together lives together.

The trade

Layered / hexagonal Vertical slice
Change locality low high
Reuse across features natural deliberate
Consistency enforcement by structure by discipline
Fits rich shared domain many independent use cases

The honest cost: each slice chooses its own tools, which is freedom and a hazard. One slice uses the ORM, another raw SQL, a third calls the service layer — fine if the differences are deliberate, a mess if they are drift. Teams that make it work agree a default and treat deviation as a decision to write down.

And the shared kernel has to be defended. Everything is “shared” to someone, and a kernel that absorbs whatever two slices happen to need becomes the layered architecture again.

In Python

python
# features/create_order/handler.py - the whole use case, one file.
router = APIRouter()

class CreateOrder(BaseModel):
    customer_id: CustomerId
    lines: list[Line]

@router.post("/orders", status_code=201)
async def create_order(cmd: CreateOrder, db: Db = Depends(get_db)) -> OrderOut:
    order = Order.create(cmd.customer_id, cmd.lines)     # shared domain
    await db.orders.add(order)                            # shared adapter
    await db.commit()
    return OrderOut.from_domain(order)

The request model, the handler and the response live together; the domain Order and the db adapter come from the kernel. A mediator library is common in the .NET world where the pattern is popular (MediatR); in Python a router per slice plus a thin kernel is enough, and adding a mediator to FastAPI is usually ceremony.

When to choose which

Reach for vertical slices when the system is mostly independent use cases with thin shared logic — an internal tool, a CRUD-heavy service, an API where each endpoint is its own small story. Reach for hexagonal when there is a genuinely rich domain that many use cases share and that must stay isolated from IO — pricing, scheduling, anything with rules worth testing on their own.

They compose: hexagonal for the kernel, slices for the use cases around it is a common and defensible shape, and saying so shows you are not picking a side for its own sake.

Where it breaks in production

  • Slices importing slices. The rule everyone breaks first, and the point at which the structure stops meaning anything.
  • A kernel that grows. Anything used twice gets promoted; a year later the kernel is the old service layer.
  • Inconsistency mistaken for freedom. Three slices, three ways to talk to the database, no decision behind any of them.
  • Cross-feature transactions. A use case that must update two slices’ data atomically has nowhere natural to live; it usually means the slice boundary is wrong.
  • Naming by layer inside the slice. features/orders/services/ recreates the layers one level down.

Interview angle 4

  • “What is vertical slice architecture?” - organising by use case instead of by layer: everything one feature needs in one folder, slices that do not import each other, and a deliberately small shared kernel underneath. It answers the layered failure where every change touches four folders.
  • “Vertical slice or hexagonal?” - by how much shared domain there is. Many independent use cases with thin logic: slices. A rich domain many use cases share and that must stay isolated from IO: hexagonal. And they compose - hexagonal kernel, slices around it.
  • “What goes wrong with it?” - slices start importing each other, the kernel absorbs everything used twice, and each slice picks its own tooling without a decision behind it. All three turn it back into a layered architecture with worse naming.
  • “Do you need a mediator?” - not in Python. A router per slice and a thin kernel is enough; MediatR-style mediation is a .NET habit that adds indirection FastAPI does not need.