Engineering Deep Dive

Microservices Architektur: Der praktische Guide für Unternehmen

Microservices Architektur verständlich erklärt: Wann sich der Umstieg lohnt, wie du ihn planst und welche Fehler du vermeiden solltest. Mit Code-Beispielen für Docker, Kubernetes und API-Gateways.

·15 Min. Lesezeit
Netzwerk- und Server-Infrastruktur als Sinnbild für Microservices Architektur

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:

KriteriumMonolithMicroservices
Komplexität am StartNiedrig -- ein Projekt, ein DeploymentHoch -- Infrastruktur, Networking, Orchestrierung
SkalierungGanze Anwendung skaliert als EinheitEinzelne Services granular skalierbar
Team-AutonomieAlle arbeiten in einer CodebasisTeams besitzen eigene Services
Deployment-RisikoEin Fehler kann alles lahmlegenFehler bleiben auf einen Service begrenzt
Technologie-FreiheitEin Tech-Stack für allesJeder Service kann anderen Stack nutzen
DebuggingEinfach -- ein Prozess, ein LogKomplex -- Distributed Tracing nötig
Daten-KonsistenzACID-Transaktionen einfachEventual Consistency, Saga-Pattern
Operative KostenGeringHoch (Kubernetes, Monitoring, Logging)
Time-to-Market (Anfang)SchnellerLangsamer durch Setup-Overhead
Time-to-Market (später)Langsamer durch wachsende AbhängigkeitenSchneller 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: 70

Beachte 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:

  1. Logging: Strukturierte JSON-Logs mit einer Correlation-ID, die über alle Services hinweg mitgegeben wird. Tools: ELK Stack, Grafana Loki.
  2. Metrics: Latenz, Durchsatz, Fehlerrate pro Service. Goldene Signale: Latency, Traffic, Errors, Saturation. Tools: Prometheus + Grafana.
  3. 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:

  1. Domain Mapping: Identifiziere die Bounded Contexts deiner Anwendung mit Domain-Driven Design. Jeder Bounded Context wird potenziell ein Service.
  2. Strangler Fig Pattern: Stelle einen API Gateway vor den Monolithen. Leite neue Features direkt an neue Services weiter. Migriere bestehende Features schrittweise.
  3. Daten entkoppeln: Der schwierigste Schritt. Jeder Service bekommt seine eigene Datenbank. Nutze Change Data Capture (CDC) für die Synchronisation während der Übergangsphase.
  4. CI/CD pro Service: Jeder Service braucht seine eigene Build- und Deployment-Pipeline. Investiere in Automatisierung.
  5. 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