Du hast den Begriff „Microservices" schon hundertmal gehört. Netflix nutzt sie. Amazon schwört darauf. Jedes zweite Tech-Unternehmen scheint seine monolithische Anwendung aufzubrechen. Aber ist das wirklich der richtige Schritt für dein Unternehmen? Oder springst du nur auf einen Hype-Zug auf, der dich mehr kostet als er bringt?
In diesem Guide gehen wir das Thema Microservices Architektur praxisnah an. Keine akademischen Definitionen, sondern echte Entscheidungshilfen, Code-Beispiele und Erfahrungswerte aus realen Projekten.
Was ist eine Microservices Architektur?
Eine Microservices Architektur zerlegt eine Anwendung in kleine, unabhängig deploybare Services. Jeder Service hat genau eine Aufgabe, seine eigene Datenbank und kommuniziert über klar definierte APIs mit anderen Services.
Stell dir vor, du baust einen Online-Shop. In einem Monolithen steckt alles in einer Codebasis: Nutzerverwaltung, Produktkatalog, Warenkorb, Zahlungsabwicklung, Versand. In einer Microservices Architektur wird jeder dieser Bereiche zu einem eigenständigen Service:
┌─────────────────────────────────────────────────────────┐
│ API Gateway │
│ (Routing, Auth, Rate Limiting) │
└──────┬──────────┬──────────┬──────────┬────────────────┘
│ │ │ │
┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼─────┐ ┌─────────┐
│ User │ │Product │ │ Cart │ │Payment │ │Shipping │
│Service │ │Service │ │Svc │ │Service │ │Service │
└───┬────┘ └───┬────┘ └──┬───┘ └───┬─────┘ └────┬────┘
│ │ │ │ │
┌───▼──┐ ┌───▼──┐ ┌──▼───┐ ┌───▼───┐ ┌───▼───┐
│UserDB│ │ProdDB│ │CartDB│ │PayDB │ │ShipDB │
└──────┘ └──────┘ └──────┘ └───────┘ └───────┘Das Prinzip dahinter: Loose Coupling, High Cohesion. Jeder Service kann unabhängig entwickelt, getestet und deployed werden. Fällt der Versand-Service aus, funktioniert der Rest der Anwendung weiterhin.
Monolith vs. Microservices: Der ehrliche Vergleich
Bevor du dich für eine Architektur entscheidest, musst du die Trade-offs kennen. Hier ist der ungeschönte Vergleich:
| Kriterium | Monolith | Microservices |
|---|---|---|
| Komplexität am Start | Niedrig -- ein Projekt, ein Deployment | Hoch -- Infrastruktur, Networking, Orchestrierung |
| Skalierung | Ganze Anwendung skaliert als Einheit | Einzelne Services granular skalierbar |
| Team-Autonomie | Alle arbeiten in einer Codebasis | Teams besitzen eigene Services |
| Deployment-Risiko | Ein Fehler kann alles lahmlegen | Fehler bleiben auf einen Service begrenzt |
| Technologie-Freiheit | Ein Tech-Stack für alles | Jeder Service kann anderen Stack nutzen |
| Debugging | Einfach -- ein Prozess, ein Log | Komplex -- Distributed Tracing nötig |
| Daten-Konsistenz | ACID-Transaktionen einfach | Eventual Consistency, Saga-Pattern |
| Operative Kosten | Gering | Hoch (Kubernetes, Monitoring, Logging) |
| Time-to-Market (Anfang) | Schneller | Langsamer durch Setup-Overhead |
| Time-to-Market (später) | Langsamer durch wachsende Abhängigkeiten | Schneller durch Unabhängigkeit |
Die Wahrheit ist: Ein gut strukturierter Monolith ist besser als schlecht gemachte Microservices. Architektur ist kein Selbstzweck -- sie muss zu deinem Problem passen.
Wann Microservices Sinn machen -- und wann nicht
Hier ist die ehrliche Entscheidungsmatrix, die wir bei unseren Software-Projekten verwenden:
Microservices sind die richtige Wahl wenn:
- Dein Team hat 20+ Entwickler und braucht unabhängige Deployment-Zyklen
- Einzelne Teile der Anwendung stark unterschiedliche Skalierungsanforderungen haben
- Du verschiedene Technologien für verschiedene Probleme nutzen willst
- Die Anwendung muss hochverfügbar sein -- Ausfälle einzelner Features dürfen nicht alles lahmlegen
- Du hast das Budget und Know-how für Kubernetes, Monitoring und DevOps
Bleib beim Monolithen wenn:
- Dein Team hat weniger als 10 Entwickler
- Du bist ein Startup und musst schnell iterieren
- Die Domäne ist noch nicht gut verstanden -- Service-Grenzen werden sich noch verschieben
- Du hast kein dediziertes DevOps/Platform-Team
- Performance-Anforderungen sind gleichmäßig über die gesamte Anwendung verteilt
Profi-Tipp: Der „Modular Monolith" ist oft der beste Startpunkt. Du strukturierst deinen Monolithen intern so, als wären es Microservices (klare Module, definierte Schnittstellen), behältst aber ein einzelnes Deployment. Wenn du später wirklich Microservices brauchst, extrahierst du Module einzeln.
Microservices in der Praxis: Docker Compose Setup
Lass uns hands-on werden. Hier ist ein realistisches Docker Compose Setup für einen E-Commerce-Stack mit mehreren Services:
# docker-compose.yml
version: "3.9"
services:
# API Gateway - zentraler Einstiegspunkt
api-gateway:
build: ./services/gateway
ports:
- "3000:3000"
environment:
- USER_SERVICE_URL=http://user-service:4001
- PRODUCT_SERVICE_URL=http://product-service:4002
- ORDER_SERVICE_URL=http://order-service:4003
- REDIS_URL=redis://redis:6379
depends_on:
- user-service
- product-service
- order-service
- redis
networks:
- backend
# User Service
user-service:
build: ./services/user
environment:
- DATABASE_URL=postgresql://user:pass@user-db:5432/users
- JWT_SECRET=${JWT_SECRET}
- RABBITMQ_URL=amqp://rabbitmq:5672
depends_on:
- user-db
- rabbitmq
networks:
- backend
# Product Service
product-service:
build: ./services/product
environment:
- DATABASE_URL=postgresql://user:pass@product-db:5432/products
- RABBITMQ_URL=amqp://rabbitmq:5672
depends_on:
- product-db
- rabbitmq
networks:
- backend
# Order Service
order-service:
build: ./services/order
environment:
- DATABASE_URL=postgresql://user:pass@order-db:5432/orders
- RABBITMQ_URL=amqp://rabbitmq:5672
- PRODUCT_SERVICE_URL=http://product-service:4002
depends_on:
- order-db
- rabbitmq
networks:
- backend
# Datenbanken - jeder Service hat seine eigene
user-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: users
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- user-data:/var/lib/postgresql/data
networks:
- backend
product-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: products
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- product-data:/var/lib/postgresql/data
networks:
- backend
order-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: orders
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- order-data:/var/lib/postgresql/data
networks:
- backend
# Message Broker für asynchrone Kommunikation
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "15672:15672" # Management UI
networks:
- backend
# Cache Layer
redis:
image: redis:7-alpine
networks:
- backend
networks:
backend:
driver: bridge
volumes:
user-data:
product-data:
order-data:Beachte die Kernprinzipien: Jeder Service hat seine eigene Datenbank (Database-per-Service Pattern), die Kommunikation läuft über ein gemeinsames Netzwerk, und RabbitMQ dient als Message Broker für asynchrone Events.
API Gateway: Das Herzstück der Kommunikation
Das API Gateway ist der zentrale Einstiegspunkt für alle Client-Anfragen. Es übernimmt Routing, Authentifizierung und Rate Limiting. Hier ein Beispiel mit Node.js und Express:
// services/gateway/src/index.ts
import express from "express";
import { createProxyMiddleware } from "http-proxy-middleware";
import rateLimit from "express-rate-limit";
import jwt from "jsonwebtoken";
const app = express();
// Rate Limiting -- schützt alle Services gleichzeitig
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 Minuten
max: 100, // max 100 Requests pro IP
standardHeaders: true,
});
app.use(limiter);
// JWT Auth Middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "Token fehlt" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(403).json({ error: "Token ungültig" });
}
}
// Health Check
app.get("/health", (req, res) => {
res.json({ status: "ok", gateway: true });
});
// Service Routing
app.use("/api/users", createProxyMiddleware({
target: process.env.USER_SERVICE_URL,
changeOrigin: true,
pathRewrite: { "^/api/users": "" },
}));
app.use("/api/products", createProxyMiddleware({
target: process.env.PRODUCT_SERVICE_URL,
changeOrigin: true,
pathRewrite: { "^/api/products": "" },
}));
// Geschützte Routes
app.use("/api/orders", authenticate, createProxyMiddleware({
target: process.env.ORDER_SERVICE_URL,
changeOrigin: true,
pathRewrite: { "^/api/orders": "" },
}));
app.listen(3000, () => {
console.log("API Gateway läuft auf Port 3000");
});In der Produktion würdest du wahrscheinlich auf ein dediziertes API Gateway wie Kong, Traefik oder AWS API Gateway setzen. Für das Verständnis der Grundlagen ist diese Express-Variante aber ideal.
Inter-Service-Kommunikation: REST und Message Queues
Services müssen miteinander reden. Dabei gibt es zwei grundlegende Muster:
Synchron: REST-Aufrufe zwischen Services
Wenn ein Service eine sofortige Antwort braucht, nutzt du direkte HTTP-Calls:
// services/order/src/product-client.ts
// Der Order-Service fragt den Product-Service nach Verfügbarkeit
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
export async function getProduct(productId: string): Promise<Product> {
const response = await fetch(
`${process.env.PRODUCT_SERVICE_URL}/products/${productId}`,
{
headers: { "Content-Type": "application/json" },
signal: AbortSignal.timeout(3000), // 3s Timeout
}
);
if (!response.ok) {
throw new Error(`Product Service Error: ${response.status}`);
}
return response.json();
}
export async function checkStock(
items: { productId: string; quantity: number }[]
): Promise<boolean> {
for (const item of items) {
const product = await getProduct(item.productId);
if (product.stock < item.quantity) return false;
}
return true;
}Asynchron: Events über Message Queues
Für Operationen, die keine sofortige Antwort brauchen, nutzt du einen Message Broker. Wenn eine Bestellung aufgegeben wird, muss der Versand-Service informiert werden -- aber nicht synchron:
// services/order/src/events.ts
import amqplib from "amqplib";
let channel: amqplib.Channel;
export async function connectQueue() {
const connection = await amqplib.connect(
process.env.RABBITMQ_URL || "amqp://localhost"
);
channel = await connection.createChannel();
// Queues deklarieren
await channel.assertQueue("order.created");
await channel.assertQueue("order.paid");
await channel.assertQueue("order.shipped");
}
// Event publizieren
export async function publishEvent(queue: string, data: unknown) {
channel.sendToQueue(queue, Buffer.from(JSON.stringify(data)), {
persistent: true,
});
}
// Event konsumieren (im Shipping-Service)
export async function consumeOrders(
handler: (order: unknown) => Promise<void>
) {
channel.consume("order.paid", async (msg) => {
if (!msg) return;
try {
const order = JSON.parse(msg.content.toString());
await handler(order);
channel.ack(msg); // Erfolgreich verarbeitet
} catch (error) {
channel.nack(msg, false, true); // Zurück in die Queue
}
});
}Wann synchron, wann asynchron? Synchron, wenn der Client auf die Antwort warten muss (z.B. Preis abfragen). Asynchron, wenn die Verarbeitung im Hintergrund passieren kann (z.B. E-Mail senden, Lager aktualisieren, Versandlabel erstellen).
Kubernetes Deployment: Production-Ready
Für den Produktionsbetrieb kommst du um Container-Orchestrierung kaum herum. Hier ist ein Kubernetes Deployment für den Product-Service:
# k8s/product-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
labels:
app: product-service
spec:
replicas: 3
selector:
matchLabels:
app: product-service
template:
metadata:
labels:
app: product-service
spec:
containers:
- name: product-service
image: registry.example.com/product-service:1.2.0
ports:
- containerPort: 4002
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: product-db-credentials
key: url
- name: RABBITMQ_URL
valueFrom:
configMapKeyRef:
name: shared-config
key: rabbitmq-url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 4002
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4002
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product-service
ports:
- port: 4002
targetPort: 4002
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: product-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: product-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70Beachte die wichtigen Production-Details: Readiness- und Liveness-Probes stellen sicher, dass nur gesunde Pods Traffic bekommen. Resource Limits verhindern, dass ein Service den ganzen Cluster lahmlegt. Und der HorizontalPodAutoscaler skaliert automatisch bei hoher CPU-Last.
Wenn du dein Unternehmen auf eine Cloud-native Architektur umstellen willst, helfen wir dir bei der Planung und Umsetzung.
Observability: Du kannst nicht fixen, was du nicht siehst
Einer der größten Unterschiede zwischen Monolith und Microservices: Debugging wird exponentiell schwieriger. Wenn ein Request durch fünf Services wandert und einer antwortet langsam, wie findest du den Flaschenhals?
Die drei Säulen der Observability:
- Logging: Strukturierte JSON-Logs mit einer Correlation-ID, die über alle Services hinweg mitgegeben wird. Tools: ELK Stack, Grafana Loki.
- Metrics: Latenz, Durchsatz, Fehlerrate pro Service. Goldene Signale: Latency, Traffic, Errors, Saturation. Tools: Prometheus + Grafana.
- Distributed Tracing: Verfolge einen Request über alle Services hinweg. Jeder Span zeigt, wie lange ein Service für seinen Teil gebraucht hat. Tools: Jaeger, Zipkin, OpenTelemetry.
// Beispiel: Correlation-ID Middleware
function correlationId(req, res, next) {
// Übernimm existierende ID oder erstelle neue
const id = req.headers["x-correlation-id"] || crypto.randomUUID();
req.correlationId = id;
res.setHeader("x-correlation-id", id);
// Bei Weiterleitung an andere Services immer mitgeben
req.serviceHeaders = {
"x-correlation-id": id,
"x-source-service": "order-service",
};
next();
}Ohne Observability fliegst du blind. Investiere mindestens 20% deines initialen Microservices-Budgets in Monitoring und Alerting.
Die häufigsten Fehler bei der Migration
Aus unserer Erfahrung mit Individualsoftware-Projekten sehen wir immer wieder dieselben Stolpersteine:
1. Zu kleine Services
Nicht jede Klasse braucht einen eigenen Service. Wenn zwei Services bei jedem Request miteinander reden müssen, gehören sie zusammen. „Nano-Services" erzeugen einen Overhead, der den Vorteil zunichtemacht.
2. Shared Database
Wenn zwei Services dieselbe Datenbank teilen, hast du keine Microservices -- du hast einen verteilten Monolithen mit extra Netzwerk-Overhead. Jeder Service braucht seinen eigenen Datenspeicher.
3. Big Bang Migration
Versuch nicht, einen Monolithen an einem Wochenende in 20 Services aufzuteilen. Extrahiere einen Service nach dem anderen, beginnend mit dem, der den meisten Schmerz verursacht (z.B. der Service, der am häufigsten deployed wird oder der am stärksten skalieren muss).
4. Kein API-Versioning
Service A ändert seine API, Service B bricht. Versioniere deine APIs von Anfang an (/api/v1/products) und plane Backward Compatibility ein.
5. Fehlendes Circuit Breaking
Wenn der Payment-Service ausfällt und der Order-Service weiter Anfragen schickt, überlastest du den ohnehin schon angeschlagenen Service. Implementiere Circuit Breaker (z.B. mit opossum für Node.js), die nach einer bestimmten Anzahl von Fehlern den Schaltkreis öffnen.
Der pragmatische Migrations-Fahrplan
Wenn du dich für Microservices entschieden hast, hier ist ein bewährter Fahrplan:
- Domain Mapping: Identifiziere die Bounded Contexts deiner Anwendung mit Domain-Driven Design. Jeder Bounded Context wird potenziell ein Service.
- Strangler Fig Pattern: Stelle einen API Gateway vor den Monolithen. Leite neue Features direkt an neue Services weiter. Migriere bestehende Features schrittweise.
- Daten entkoppeln: Der schwierigste Schritt. Jeder Service bekommt seine eigene Datenbank. Nutze Change Data Capture (CDC) für die Synchronisation während der Übergangsphase.
- CI/CD pro Service: Jeder Service braucht seine eigene Build- und Deployment-Pipeline. Investiere in Automatisierung.
- Observability First: Logging, Metrics und Tracing müssen stehen, bevor du den zweiten Service extrahierst. Nicht danach.
Die Migration zu Microservices ist ein Marathon, kein Sprint. Plane 6-18 Monate für eine signifikante Migration ein. Und rechne damit, dass du unterwegs Service-Grenzen anpassen wirst.
Fazit: Architektur als strategische Entscheidung
Microservices Architektur ist kein Silver Bullet. Sie löst echte Probleme -- aber sie schafft auch neue. Die richtige Frage ist nicht „Sollen wir Microservices nutzen?", sondern „Welche konkreten Probleme wollen wir lösen, und ist eine verteilte Architektur der beste Weg dorthin?"
Wenn dein Team wächst, dein Monolith unter der Last ächzt und verschiedene Teile deiner Anwendung unterschiedlich skalieren müssen -- dann sind Microservices ein mächtiges Werkzeug. Aber geh es schrittweise an, investiere in Observability und starte mit einem Modular Monolith, wenn du noch am Anfang stehst.
Die Technologie ist nur ein Teil der Gleichung. Organisationsstruktur, Team-Kultur und operative Reife sind genauso entscheidend. Conway's Law gilt auch hier: Deine Architektur wird die Kommunikationsstruktur deines Teams widerspiegeln.
Bereit für den nächsten Schritt?
Ob Microservices Migration, Cloud-Architektur oder Modernisierung deiner bestehenden Systeme -- wir beraten dich ehrlich und setzen gemeinsam um. Kein Buzzword-Bingo, nur Ergebnisse.
Unverbindlich anfragen