Beyond Layered Architectures Crafting Scalable APIs with Vertical Slices in FastAPI
Min-jun Kim
Dev Intern · Leapcell

Introduction
For decades, the layered architecture has been the bedrock of backend system design. We've become accustomed to the strict separation of concerns into presentation, business logic, and data access layers. While this approach offers clear structural advantages and promotes code organization, the complexity of modern applications often exposes its limitations. As our services grow, a seemingly simple new feature can ripple through all layers, leading to extensive cross-cutting changes, increased cognitive load, and slower development cycles. This article delves into an alternative paradigm gaining traction in the backend world: Vertical Slice Architecture. We will explore how this approach, when applied to a modern framework like FastAPI, can lead to more focused, maintainable, and ultimately, more scalable API services, paving the way for a fresh perspective on API design.
Core Concepts Explained
Before diving into the practicalities of vertical slices, let's establish a clear understanding of the core terms we'll be discussing.
Layered Architecture: This traditional architectural style organizes code into distinct horizontal layers, each with a specific responsibility. For instance, a typical web application might have a presentation layer (controllers/routers), a business logic layer (services), and a data access layer (repositories). Communication generally flows downwards, and each layer is largely unaware of the implementation details of the layers above it.
Vertical Slice Architecture (VSA): Drastically different from layered approaches, VSA organizes code around distinct features or use cases, often referred to as "vertical slices" or "features." Each slice encapsulates all the components necessary to deliver a specific piece of functionality, from API endpoint definition to data persistence. Imagine slicing a cake vertically – each slice contains a bit of every layer.
Domain-Driven Design (DDD): While not strictly tied to VSA, DDD principles often complement it beautifully. DDD emphasizes understanding the business domain deeply and modeling software closely to that domain. VSA's focus on feature-centric development aligns well with DDD's emphasis on ubiquitous language and bounded contexts for each domain concern.
CQRS (Command Query Responsibility Segregation): In some VSA implementations, CQRS can be a natural fit. It suggests separating the concerns of modifying data (commands) from querying data (queries). Within a vertical slice, you might find distinct command handlers and query handlers managing the respective operations for that specific feature.
The Principle of Vertical Slices
The core principle behind Vertical Slice Architecture is to abandon the horizontal layering for a vertical, feature-centric organization. Instead of having a services directory containing all business logic for the entire application, and a repositories directory for all data access, VSA suggests grouping all related code for a single feature together.
For example, consider an application managing user profiles. In a layered architecture, you might have:
app/api/endpoints/users.py(FastAPI router)app/services/user_service.py(Business logic)app/repositories/user_repository.py(Data access)app/schemas/user_schemas.py(Pydantic models)
In a Vertical Slice Architecture, all of these components for the "Create User" feature might reside within a single directory, say app/features/create_user/. This directory would contain the endpoint definition, the request/response models, the business logic, and even the data persistence logic for creating a user.
The benefits are numerous:
- Reduced Cognitive Load: When working on a feature, all relevant code is in one place. You don't have to jump between multiple directories and files across different layers.
- Increased Cohesion: Components within a slice are highly cohesive and directly contribute to a single feature.
- Decoupling: Slices are largely independent. Changes within one slice are less likely to impact other slices, reducing the risk of regressions.
- Easier Testing: Each slice can be tested in isolation, as its dependencies are self-contained or explicitly managed within the slice.
- Simplified Onboarding: New developers can grasp individual features more quickly by focusing on a single, self-contained unit of code.
- Better Scalability and Maintainability: As the application grows, adding new features becomes adding new slices, rather than modifying or extending existing, potentially monolithic layers.
Implementing Vertical Slices in FastAPI
Let's illustrate how to implement VSA within a FastAPI application with a practical example. We'll consider a simple e-commerce application with a Product entity. We'll focus on two features: "Create Product" and "Get Product by ID."
First, let's define our project structure based on vertical slices:
├── app/
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── features/
│ │ ├── create_product/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py # FastAPI router defines endpoint
│ │ │ ├── schemas.py # Pydantic models for request/response
│ │ │ ├── service.py # Business logic for creating a product
│ │ │ └── repository.py # Data access logic specific to creating a product
│ │ ├── get_product_by_id/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py
│ │ │ ├── schemas.py
│ │ │ ├── service.py
│ │ │ └── repository.py
│ └── __init__.py
Let's look at the code for the create_product slice.
app/models.py (Shared database model, though VSA aims to reduce such broad shared components, sometimes core domain models are shared)
from sqlalchemy import Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Product(Base): __tablename__ = "products" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) description = Column(String) price = Column(Float) def to_dict(self): return { "id": self.id, "name": self.name, "description": self.description, "price": self.price, }
app/database.py (Shared database setup)
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker DATABASE_URL = "sqlite:///./test.db" engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close()
app/features/create_product/schemas.py
from pydantic import BaseModel class ProductCreate(BaseModel): name: str description: str | None = None price: float class ProductResponse(BaseModel): id: int name: str description: str | None = None price: float class Config: from_attributes = True # Pydantic v2 # orm_mode = True # Pydantic v1
app/features/create_product/repository.py
from sqlalchemy.orm import Session from app.models import Product from app.features.create_product.schemas import ProductCreate def create_product(db: Session, product: ProductCreate) -> Product: db_product = Product(name=product.name, description=product.description, price=product.price) db.add(db_product) db.commit() db.refresh(db_product) return db_product
app/features/create_product/service.py
from sqlalchemy.orm import Session from app.features.create_product import repository from app.features.create_product.schemas import ProductCreate, ProductResponse def create_new_product(db: Session, product_data: ProductCreate) -> ProductResponse: # Here you can add any business logic before or after calling the repository # validate price, check inventory, apply discounts, etc. db_product = repository.create_product(db, product_data) return ProductResponse.model_validate(db_product)
app/features/create_product/endpoint.py
from fastapi import APIRouter, Depends, status from sqlalchemy.orm import Session from app.database import get_db from app.features.create_product import service from app.features.create_product.schemas import ProductCreate, ProductResponse router_create_product = APIRouter(tags=["Products"]) @router_create_product.post("/products/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) def create_product_endpoint(product: ProductCreate, db: Session = Depends(get_db)): return service.create_new_product(db, product)
Now, the get_product_by_id slice:
app/features/get_product_by_id/schemas.py
from pydantic import BaseModel from app.features.create_product.schemas import ProductResponse # Reusing for consistency, but could be specific # No specific request schema needed for a simple GET by ID # ProductResponse can be reused from create_product slice if identical
app/features/get_product_by_id/repository.py
from sqlalchemy.orm import Session from app.models import Product def get_product(db: Session, product_id: int) -> Product | None: return db.query(Product).filter(Product.id == product_id).first()
app/features/get_product_by_id/service.py
from fastapi import HTTPException, status from sqlalchemy.orm import Session from app.features.get_product_by_id import repository from app.features.create_product.schemas import ProductResponse # Reusing schema def retrieve_product_by_id(db: Session, product_id: int) -> ProductResponse: product = repository.get_product(db, product_id) if product is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Product with id {product_id} not found" ) return ProductResponse.model_validate(product)
app/features/get_product_by_id/endpoint.py
from fastapi import APIRouter, Depends, status, HTTPException from sqlalchemy.orm import Session from app.database import get_db from app.features.get_product_by_id import service from app.features.create_product.schemas import ProductResponse # Reusing schema router_get_product_by_id = APIRouter(tags=["Products"]) @router_get_product_by_id.get("/products/{product_id}", response_model=ProductResponse) def get_product_endpoint(product_id: int, db: Session = Depends(get_db)): return service.retrieve_product_by_id(db, product_id)
Finally, connecting everything in app/main.py:
from fastapi import FastAPI from app.database import Base, engine from app.features.create_product.endpoint import router_create_product from app.features.get_product_by_id.endpoint import router_get_product_by_id from app.models import Product # Ensure models are imported for Base.metadata.create_all Base.metadata.create_all(bind=engine) app = FastAPI(title="Vertical Slice Product API") app.include_router(router_create_product) app.include_router(router_get_product_by_id) @app.get("/") async def root(): return {"message": "Welcome to Vertical Slice Product API"}
In this setup, each feature directory (create_product, get_product_by_id) acts as a self-contained unit. When you need to modify how a product is created, you only need to touch files within the create_product directory. While some components like app/models.py and app/database.py are still shared, the goal is to minimize such shared entities and encapsulate as much as possible within each slice. This keeps the impact of changes localized.
Application Scenarios
Vertical Slice Architecture shines in several scenarios:
- Microservices or Monoliths with Micro-boundaries: VSA provides clear feature boundaries, making it easier to extract specific slices into new microservices if needed, or maintain a monolithic application with microservice-like internal organization.
- Teams Working on Different Features: When multiple teams are working on distinct features, VSA minimizes merge conflicts and allows teams to operate with greater autonomy.
- Complex Business Domains: For applications with rich and intricate business logic, VSA helps manage complexity by breaking down the domain into manageable, problem-specific slices.
- Rapid Prototyping and Iteration: The self-contained nature of slices allows for quicker development and deployment of individual features.
- Event-Driven Architectures: Each slice can define its own event producers and consumers, simplifying event-driven communication patterns.
Conclusion
The pursuit of better architectural patterns is a continuous journey in software development. While layered architectures have served us well, the Vertical Slice Architecture offers a refreshing and highly effective alternative, particularly for modern, rapidly evolving applications. By focusing on feature-centric development, VSA in FastAPI promotes highly cohesive, loosely coupled, and independently deployable units of functionality. This paradigm shift can significantly enhance maintainability, reduce cognitive load, and accelerate development, making it a compelling choice for crafting scalable and robust API services. Embrace vertical slices, and build systems that are truly aligned with your business capabilities.

