Jenseits von geschichteten Architekturen: Skalierbare APIs mit vertikalen Slices in FastAPI erstellen
Min-jun Kim
Dev Intern · Leapcell

Jenseits von geschichteten Architekturen: Skalierbare APIs mit vertikalen Slices in FastAPI erstellen
Einleitung
Seit Jahrzehnten ist die geschichtete Architektur das Fundament des Backend-Systemdesigns. Wir haben uns an die strikte Trennung von Verantwortlichkeiten in Präsentations-, Geschäftslogik- und Datenzugriffsschichten gewöhnt. Während dieser Ansatz klare strukturelle Vorteile bietet und die Codeorganisation fördert, deckt die Komplexität moderner Anwendungen oft dessen Grenzen auf. Wenn unsere Dienste wachsen, kann eine scheinbar einfache neue Funktion durch alle Schichten sickern, was zu umfangreichen übergreifenden Änderungen, erhöhter kognitiver Belastung und langsameren Entwicklungszyklen führt. Dieser Artikel befasst sich mit einem alternativen Paradigma, das in der Backend-Welt an Bedeutung gewinnt: der Vertical Slice Architecture. Wir werden untersuchen, wie dieser Ansatz, wenn er auf ein modernes Framework wie FastAPI angewendet wird, zu fokussierteren, wartungsfreundlicheren und letztendlich skalierbareren API-Diensten führen kann und den Weg für eine neue Perspektive auf API-Design ebnet.
Kernkonzepte erklärt
Bevor wir uns mit den praktischen Aspekten von vertikalen Slices befassen, wollen wir ein klares Verständnis der Kernbegriffe entwickeln, die wir diskutieren werden.
Geschichtete Architektur: Dieser traditionelle architektonische Stil organisiert Code in verschiedene horizontale Schichten, von denen jede eine bestimmte Verantwortung hat. Eine typische Webanwendung könnte beispielsweise eine Präsentationsschicht (Controller/Router), eine Geschäftslogikschicht (Services) und eine Datenzugriffsschicht (Repositories) haben. Die Kommunikation fließt im Allgemeinen nach unten, und jede Schicht ist sich der Implementierungsdetails der darüber liegenden Schichten weitgehend unbekannt.
Vertical Slice Architecture (VSA): Drastisch anders als geschichtete Ansätze, organisiert VSA Code um verschiedene Funktionen oder Anwendungsfälle, die oft als "vertikale Slices" oder "Features" bezeichnet werden. Jede Slice kapselt alle Komponenten, die zur Bereitstellung eines bestimmten Funktionalitätsstücks erforderlich sind, von der API-Endpunktdefinition bis zur Datenpersistenz. Stellen Sie sich vor, Sie schneiden einen Kuchen vertikal – jede Scheibe enthält ein Stück jeder Schicht.
Domain-Driven Design (DDD): Obwohl nicht streng an VSA gebunden, ergänzen sich DDD-Prinzipien oft schön. DDD betont das tiefe Verständnis der Geschäftsdomäne und die Modellierung von Software eng an dieser Domäne. Der Fokus von VSA auf die Feature-zentrierte Entwicklung passt gut zur Betonung von DDD auf universelle Sprache und begrenzte Kontexte für jedes Domänenanliegen.
CQRS (Command Query Responsibility Segregation): In einigen VSA-Implementierungen kann CQRS eine natürliche Passform sein. Es schlägt vor, die Verantwortlichkeiten für das Ändern von Daten (Befehle) von der Abfrage von Daten (Abfragen) zu trennen. Innerhalb eines vertikalen Slices finden Sie möglicherweise separate Befehls- und Abfragehandler, die die jeweiligen Operationen für diese spezielle Funktion verwalten.
Das Prinzip der vertikalen Slices
Das Kernprinzip hinter der Vertical Slice Architecture ist die Aufgabe der horizontalen Schichtung zugunsten einer vertikalen, Feature-zentrierten Organisation. Anstatt ein Verzeichnis services zu haben, das die gesamte Geschäftslogik für die gesamte Anwendung enthält, und ein Verzeichnis repositories für den gesamten Datenzugriff, schlägt VSA vor, allen verwandten Code für eine einzelne Funktion zusammenzufassen.
Betrachten Sie beispielsweise eine Anwendung zur Verwaltung von Benutzerprofilen. In einer geschichteten Architektur hätten Sie möglicherweise:
app/api/endpoints/users.py(FastAPI-Router)app/services/user_service.py(Geschäftslogik)app/repositories/user_repository.py(Datenzugriff)app/schemas/user_schemas.py(Pydantic-Modelle)
In einer Vertical Slice Architecture könnten alle diese Komponenten für die Funktion "Benutzer erstellen" in einem einzigen Verzeichnis liegen, z. B. app/features/create_user/. Dieses Verzeichnis würde die Endpunktdaten, die Anfrage-/Antwortmodelle, die Geschäftslogik und sogar die Datenpersistenzlogik für die Erstellung eines Benutzers enthalten.
Die Vorteile sind zahlreich:
- Reduzierte kognitive Belastung: Beim Arbeiten an einer Funktion befindet sich der gesamte relevante Code an einem Ort. Sie müssen nicht zwischen mehreren Verzeichnissen und Dateien in verschiedenen Ebenen springen.
- Erhöhte Kohäsion: Komponenten innerhalb eines Slices sind hochgradig kohäsiv und tragen direkt zu einer einzelnen Funktion bei.
- Entkopplung: Slices sind weitgehend unabhängig. Änderungen innerhalb eines Slices beeinflussen andere Slices wahrscheinlich weniger, wodurch das Risiko von Deregressionen verringert wird.
- Einfachere Tests: Jede Slice kann isoliert getestet werden, da ihre Abhängigkeiten in sich geschlossen sind oder innerhalb der Slice explizit verwaltet werden.
- Vereinfachtes Onboarding: Neue Entwickler können einzelne Funktionen schneller erfassen, indem sie sich auf eine einzige, in sich geschlossene Codeeinheit konzentrieren.
- Bessere Skalierbarkeit und Wartbarkeit: Wenn die Anwendung wächst, wird das Hinzufügen neuer Funktionen zum Hinzufügen neuer Slices, anstatt bestehende, potenziell monolithische Schichten zu ändern oder zu erweitern.
Vertikale Slices in FastAPI implementieren
Lassen Sie uns anhand eines praktischen Beispiels veranschaulichen, wie VSA in einer FastAPI-Anwendung implementiert wird. Wir betrachten eine einfache E-Commerce-Anwendung mit einer Product-Entität. Wir konzentrieren uns auf zwei Funktionen: "Produkt erstellen" und "Produkt anhand der ID abrufen".
Zuerst definieren wir unsere Projektstruktur basierend auf vertikalen Slices:
├── app/
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── features/
│ │ ├── create_product/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py # FastAPI-Router definiert Endpunkt
│ │ │ ├── schemas.py # Pydantic-Modelle für Anfrage/Antwort
│ │ │ ├── service.py # Geschäftslogik für die Erstellung eines Produkts
│ │ │ └── repository.py # Datenzugriffslogik spezifisch für die Erstellung eines Produkts
│ │ ├── get_product_by_id/
│ │ │ ├── __init__.py
│ │ │ ├── endpoint.py
│ │ │ ├── schemas.py
│ │ │ ├── service.py
│ │ │ └── repository.py
│ └── __init__.py
Schauen wir uns den Code für die create_product-Slice an.
app/models.py (Gemeinsames Datenbankmodell, obwohl VSA darauf abzielt, solche breit geteilten Komponenten zu reduzieren, werden manchmal Kern-Domänenmodelle geteilt)
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 (Gemeinsame Datenbankeinrichtung)
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: # Hier können Sie vor oder nach dem Aufruf des Repositories jegliche Geschäftslogik hinzufügen # Preis validieren, Lagerbestand prüfen, Rabatte anwenden usw. 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)
Nun die 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 # Zur Konsistenz wiederverwenden, könnte aber spezifisch sein # Keine spezifische Anfrage-Schema für ein einfaches GET nach ID benötigt # ProductResponse kann aus der create_product-Slice wiederverwendet werden, wenn identisch
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 # Schema wiederverwenden 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"Produkt mit ID {product_id} nicht gefunden" ) 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 # Schema wiederverwenden 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)
Schließlich verbinden wir alles 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 # Sicherstellen, dass Modelle für Base.metadata.create_all importiert werden 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": "Willkommen bei der Vertical Slice Product API"}
In diesem Setup fungiert jedes Feature-Verzeichnis (create_product, get_product_by_id) als in sich geschlossene Einheit. Wenn Sie ändern müssen, wie ein Produkt erstellt wird, müssen Sie nur Dateien im Verzeichnis create_product berühren. Während einige Komponenten wie app/models.py und app/database.py immer noch geteilt werden, besteht das Ziel darin, solche geteilten Entitäten zu minimieren und so viel wie möglich innerhalb jedes Slices zu kapseln. Dies hält die Auswirkungen von Änderungen lokalisiert.
Anwendungsfälle
Die Vertical Slice Architecture glänzt in mehreren Szenarien:
- Microservices oder Monolithen mit Micro-Grenzen: VSA bietet klare Feature-Grenzen, wodurch es einfacher wird, bestimmte Slices bei Bedarf in neue Microservices zu extrahieren oder eine monolithische Anwendung mit einer internen Organisation im Microservice-Stil beizubehalten.
- Teams, die an verschiedenen Features arbeiten: Wenn mehrere Teams an verschiedenen Funktionen arbeiten, minimiert VSA Merge-Konflikte und ermöglicht es den Teams, mit größerer Autonomie zu arbeiten.
- Komplexe Geschäftsdomänen: Für Anwendungen mit reichhaltiger und komplexer Geschäftslogik hilft VSA, die Komplexität zu bewältigen, indem die Domäne in überschaubare, problemorientierte Slices unterteilt wird.
- Schnelle Prototypen und Iterationen: Die in sich geschlossene Natur von Slices ermöglicht eine schnellere Entwicklung und Bereitstellung einzelner Funktionen.
- Ereignisgesteuerte Architekturen: Jede Slice kann ihre eigenen Ereignisproduzenten und -verbraucher definieren, was ereignisgesteuerte Kommunikationsmuster vereinfacht.
Fazit
Das Streben nach besseren Architekturmustern ist eine kontinuierliche Reise in der Softwareentwicklung. Während geschichtete Architekturen uns gute Dienste geleistet haben, bietet die Vertical Slice Architecture eine erfrischende und sehr effektive Alternative, insbesondere für moderne, sich schnell entwickelnde Anwendungen. Durch die Konzentration auf Feature-zentrierte Entwicklung fördert VSA in FastAPI hochgradig kohäsive, lose gekoppelte und unabhängig bereitstellbare Funktionseinheiten. Dieser Paradigmenwechsel kann die Wartbarkeit erheblich verbessern, die kognitive Belastung reduzieren und die Entwicklung beschleunigen, was ihn zu einer überzeugenden Wahl für die Erstellung skalierbarer und robuster API-Dienste macht. Nutzen Sie vertikale Slices und bauen Sie Systeme, die wirklich auf Ihre Geschäftsmöglichkeiten abgestimmt sind.

