Vertical Slice Architecture: Organizing by Functionality
architecturevertical-sliceclean-architecture

What is Vertical Slice Architecture?
Instead of organizing code by technical layers (Controllers, Services, Repositories), Vertical Slice Architecture organizes by functionality or feature. Each "slice" contains everything needed for a specific operation.
flowchart TB
subgraph Traditional["Layered Architecture"]
direction TB
C[Controllers Layer]
S[Services Layer]
R[Repositories Layer]
C --> S --> R
end
subgraph Vertical["Vertical Slice"]
direction TB
F1[Feature: CreateOrder]
F2[Feature: GetOrder]
F3[Feature: CancelOrder]
endLayered Architecture vs Vertical Slice
Problem with Traditional Layers
src/
├── controllers/
│ ├── OrderController.ts # Many mixed methods
│ ├── ProductController.ts
│ └── UserController.ts
├── services/
│ ├── OrderService.ts # Cross dependencies
│ ├── ProductService.ts
│ └── UserService.ts
├── repositories/
│ ├── OrderRepository.ts
│ ├── ProductRepository.ts
│ └── UserRepository.ts
└── models/
├── Order.ts
├── Product.ts
└── User.tsSolution: Vertical Slices
src/
├── features/
│ ├── orders/
│ │ ├── create-order/
│ │ │ ├── CreateOrderCommand.ts
│ │ │ ├── CreateOrderHandler.ts
│ │ │ └── CreateOrderValidator.ts
│ │ ├── get-order/
│ │ │ ├── GetOrderQuery.ts
│ │ │ └── GetOrderHandler.ts
│ │ └── cancel-order/
│ │ ├── CancelOrderCommand.ts
│ │ └── CancelOrderHandler.ts
│ └── products/
│ ├── create-product/
│ └── list-products/
└── shared/
├── database/
└── middleware/Practical Implementation
1. Define the Slice
// features/orders/create-order/CreateOrderCommand.ts
export interface CreateOrderCommand {
customerId: string;
items: Array<{
productId: string;
quantity: number;
}>;
shippingAddress: Address;
}
export interface CreateOrderResult {
orderId: string;
total: number;
estimatedDelivery: Date;
}2. Handler with All Logic
// features/orders/create-order/CreateOrderHandler.ts
import { prisma } from '@/shared/database';
import { CreateOrderCommand, CreateOrderResult } from './CreateOrderCommand';
import { validateOrder } from './CreateOrderValidator';
export async function handleCreateOrder(
command: CreateOrderCommand
): Promise<CreateOrderResult> {
// 1. Validation
const validation = validateOrder(command);
if (!validation.success) {
throw new ValidationError(validation.errors);
}
// 2. Check inventory
const products = await prisma.product.findMany({
where: {
id: { in: command.items.map(i => i.productId) }
}
});
for (const item of command.items) {
const product = products.find(p => p.id === item.productId);
if (!product || product.stock < item.quantity) {
throw new InsufficientStockError(item.productId);
}
}
// 3. Calculate total
const total = command.items.reduce((sum, item) => {
const product = products.find(p => p.id === item.productId)!;
return sum + (product.price * item.quantity);
}, 0);
// 4. Create order in transaction
const order = await prisma.$transaction(async (tx) => {
// Create order
const order = await tx.order.create({
data: {
customerId: command.customerId,
total,
status: 'PENDING',
items: {
create: command.items.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: products.find(p => p.id === item.productId)!.price
}))
}
}
});
// Update inventory
for (const item of command.items) {
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
}
return order;
});
return {
orderId: order.id,
total: order.total,
estimatedDelivery: calculateDeliveryDate(command.shippingAddress)
};
}3. Endpoint Using the Handler
// app/api/orders/route.ts (Next.js App Router)
import { handleCreateOrder } from '@/features/orders/create-order/CreateOrderHandler';
export async function POST(request: Request) {
const command = await request.json();
try {
const result = await handleCreateOrder(command);
return Response.json(result, { status: 201 });
} catch (error) {
if (error instanceof ValidationError) {
return Response.json({ errors: error.errors }, { status: 400 });
}
throw error;
}
}With CQRS (Command Query Responsibility Segregation)
flowchart LR
subgraph Commands
C1[CreateOrder]
C2[CancelOrder]
C3[UpdateOrder]
end
subgraph Queries
Q1[GetOrder]
Q2[ListOrders]
Q3[GetOrderHistory]
end
Commands --> WriteDB[(Write DB)]
Queries --> ReadDB[(Read DB)]// Mediator to dispatch commands/queries
class Mediator {
private handlers = new Map<string, Handler>();
register<T, R>(type: string, handler: Handler<T, R>): void {
this.handlers.set(type, handler);
}
async send<T, R>(request: T & { type: string }): Promise<R> {
const handler = this.handlers.get(request.type);
if (!handler) throw new Error(`No handler for ${request.type}`);
return handler.handle(request);
}
}
// Usage
const mediator = new Mediator();
mediator.register('CreateOrder', new CreateOrderHandler());
mediator.register('GetOrder', new GetOrderHandler());
// In endpoint
const result = await mediator.send({
type: 'CreateOrder',
customerId: '123',
items: [...]
});Comparison
| Aspect | Layers | Vertical Slice |
|---|---|---|
| Cohesion | Low (by tech) | High (by feature) |
| Coupling | High between layers | Low between slices |
| Changes | Touch multiple layers | Touch one slice |
| Testing | Dispersed tests | Focused tests |
| Onboarding | Hard to follow | Easy to understand |
When to Use
| Situation | Recommendation |
|---|---|
| Independent features | ✅ Ideal |
| Feature teams | ✅ Very useful |
| Simple CRUD | ❌ Overkill |
| Much shared logic | ⚠️ Evaluate |
Conclusion
Vertical Slice Architecture aligns code structure with the business domain, making it easier to understand, modify, and scale each functionality independently.