Idempotenz in API Design | Enjyn Gruppe
Hallo Welt
Hallo Welt
Original Lingva Deutsch
Übersetzung wird vorbereitet...
Dieser Vorgang kann bis zu 60 Sekunden dauern.
Diese Seite wird erstmalig übersetzt und dann für alle Besucher gespeichert.
0%
DE Zurück zu Deutsch
Übersetzung durch Lingva Translate

235 Dokumentationen verfügbar

Wissensdatenbank

Idempotenz API Design

Zuletzt aktualisiert: 20.01.2026 um 11:26 Uhr

Idempotenz in API Design

Idempotente APIs können sicher mehrfach aufgerufen werden ohne unerwünschte Nebeneffekte. Lernen Sie Idempotenz richtig zu implementieren für robuste und fehlertolerante APIs.

Was ist Idempotenz?

┌─────────────────────────────────────────────────────────────┐
│                     IDEMPOTENZ                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   "Eine Operation ist idempotent, wenn sie mehrfach        │
│    ausgeführt werden kann und das gleiche Ergebnis         │
│    liefert wie eine einzige Ausführung."                   │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │                                                      │  │
│   │   f(x) = f(f(x)) = f(f(f(x))) = ...                │  │
│   │                                                      │  │
│   │   1 Aufruf = 2 Aufrufe = n Aufrufe                  │  │
│   │                                                      │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
│   BEISPIEL IDEMPOTENT:                                     │
│   PUT /users/123 { "name": "John" }                        │
│   → Ausführung 1x: User heißt John                        │
│   → Ausführung 3x: User heißt immer noch John ✅          │
│                                                             │
│   BEISPIEL NICHT IDEMPOTENT:                               │
│   POST /orders { "product": "Laptop" }                     │
│   → Ausführung 1x: 1 Bestellung                           │
│   → Ausführung 3x: 3 Bestellungen ❌                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

HTTP Methoden und Idempotenz

Methode Idempotent Safe Erklärung
GET ✅ Ja ✅ Ja Liest nur, ändert nichts
HEAD ✅ Ja ✅ Ja Wie GET ohne Body
PUT ✅ Ja ❌ Nein Ersetzt komplette Ressource
DELETE ✅ Ja ❌ Nein Ressource ist danach weg
POST ❌ Nein ❌ Nein Erstellt neue Ressource
PATCH ❌ Nein* ❌ Nein Teilweise Updates

* PATCH kann idempotent implementiert werden, ist es aber nicht per Definition.

Warum ist Idempotenz wichtig?

SZENARIO: Netzwerk-Timeout bei Zahlung

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   Client                    Server                          │
│     │                         │                             │
│     │── POST /payments ──────►│                             │
│     │   { amount: 100 }       │                             │
│     │                         │ ┌──────────────────────┐   │
│     │                         │ │ Zahlung verarbeitet  │   │
│     │                         │ │ 100€ abgebucht       │   │
│     │                         │ └──────────────────────┘   │
│     │                         │                             │
│     │◄── TIMEOUT ────────────│  Response verloren!         │
│     │                         │                             │
│     │                         │                             │
│     │── POST /payments ──────►│  Client versucht erneut    │
│     │   { amount: 100 }       │                             │
│     │                         │ ┌──────────────────────┐   │
│     │                         │ │ NOCH MAL verarbeitet │   │
│     │                         │ │ 100€ ERNEUT abgebucht│   │
│     │                         │ └──────────────────────┘   │
│     │                         │                             │
│     │◄── 201 Created ────────│                             │
│                                                             │
│   RESULTAT: Kunde hat 200€ statt 100€ bezahlt! 💸          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Idempotency Key Pattern

// Client sendet eindeutigen Key mit Request
POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
    "amount": 100,
    "currency": "EUR",
    "customer_id": 123
}

// Server speichert Key + Response
// Bei wiederholtem Request: Cached Response zurückgeben
// PHP Implementation

class IdempotencyMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        // Nur für POST Requests
        if ($request->method() !== 'POST') {
            return $next($request);
        }

        $idempotencyKey = $request->header('Idempotency-Key');

        if (!$idempotencyKey) {
            return $next($request);  // Ohne Key normal verarbeiten
        }

        // Prüfen ob Request schon verarbeitet wurde
        $cached = Cache::get("idempotency:{$idempotencyKey}");

        if ($cached) {
            // Gleicher Request? Cached Response zurückgeben
            if ($this->requestMatches($request, $cached['request'])) {
                return response()->json(
                    $cached['response'],
                    $cached['status']
                )->header('Idempotent-Replayed', 'true');
            }

            // Anderer Request mit gleichem Key = Fehler
            return response()->json([
                'error' => 'Idempotency key already used for different request'
            ], 422);
        }

        // Request verarbeiten
        $response = $next($request);

        // Response cachen (24 Stunden)
        Cache::put("idempotency:{$idempotencyKey}", [
            'request' => $this->hashRequest($request),
            'response' => $response->getData(),
            'status' => $response->status()
        ], 86400);

        return $response;
    }

    private function hashRequest(Request $request): string
    {
        return hash('sha256', json_encode([
            'path' => $request->path(),
            'body' => $request->all()
        ]));
    }

    private function requestMatches(Request $request, string $hash): bool
    {
        return $this->hashRequest($request) === $hash;
    }
}

Idempotent POST mit Client-ID

// Alternative: Client generiert ID

POST /api/orders
Content-Type: application/json

{
    "id": "ord_550e8400-e29b-41d4-a716-446655440000",  // Client-generiert
    "product_id": 123,
    "quantity": 2
}

// Server: INSERT ... ON CONFLICT DO NOTHING
// oder: Prüfen ob ID existiert

// Response bei Wiederholung:
// 200 OK (existierende Bestellung)
// statt
// 201 Created (neue Bestellung)
// SQL mit UPSERT
INSERT INTO orders (id, product_id, quantity, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (id) DO NOTHING
RETURNING *;

// PHP Implementation
class OrderController
{
    public function store(Request $request): JsonResponse
    {
        $orderId = $request->input('id');

        // Prüfen ob Bestellung existiert
        $existingOrder = Order::find($orderId);

        if ($existingOrder) {
            // Idempotent: Existierende Bestellung zurückgeben
            return response()->json($existingOrder, 200)
                ->header('Idempotent-Replayed', 'true');
        }

        // Neue Bestellung erstellen
        $order = Order::create([
            'id' => $orderId,
            'product_id' => $request->input('product_id'),
            'quantity' => $request->input('quantity')
        ]);

        return response()->json($order, 201);
    }
}

Idempotentes DELETE

// DELETE ist per Definition idempotent
// ABER: Was wenn Ressource nicht existiert?

// Option 1: 404 Not Found
DELETE /api/users/999  →  404 Not Found (nicht idempotent!)

// Option 2: 204 No Content (auch wenn nicht existiert)
DELETE /api/users/999  →  204 No Content (idempotent!)

// Empfehlung: 204 No Content
// "Die Ressource existiert nicht (mehr)" = Erfolgreich
// PHP Implementation
class UserController
{
    public function destroy(int $id): Response
    {
        // Soft Delete oder Hard Delete
        $deleted = User::where('id', $id)->delete();

        // Immer 204, egal ob gelöscht oder nicht existiert
        return response()->noContent();  // 204

        // NICHT:
        // if (!$deleted) return response()->json(['error' => 'Not found'], 404);
    }
}

Idempotentes PATCH

// PATCH kann idempotent gemacht werden

// ❌ Nicht idempotent: Relative Änderung
PATCH /api/accounts/123
{ "balance_change": +100 }

1x ausführen: Balance = 1000 + 100 = 1100
2x ausführen: Balance = 1100 + 100 = 1200 ❌

// ✅ Idempotent: Absolute Änderung
PATCH /api/accounts/123
{ "balance": 1100 }

1x ausführen: Balance = 1100
2x ausführen: Balance = 1100 ✅
// Idempotentes Increment mit Version/ETag

PATCH /api/accounts/123
If-Match: "v42"
Content-Type: application/json

{ "balance": 1100 }

// Server prüft Version
// Wenn v42 nicht aktuell: 412 Precondition Failed
// Client muss neu laden und Änderung neu berechnen

Retry-Strategien

// Client-seitige Retry mit Idempotency Key

async function createPayment(data, maxRetries = 3) {
    const idempotencyKey = crypto.randomUUID();

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch('/api/payments', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Idempotency-Key': idempotencyKey
                },
                body: JSON.stringify(data)
            });

            if (response.ok) {
                return await response.json();
            }

            // Nicht retrybare Fehler
            if (response.status >= 400 && response.status < 500) {
                throw new Error(`Client error: ${response.status}`);
            }

            // Server-Fehler: Retry
        } catch (error) {
            if (attempt === maxRetries) throw error;

            // Exponential Backoff
            await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
        }
    }
}

Best Practices

1. IDEMPOTENCY KEY
   • UUID/ULID verwenden
   • Client generiert Key
   • 24-48 Stunden cachen
   • Bei Konflikt: 409 oder 422

2. RESPONSE CACHING
   • Request Hash speichern
   • Gleicher Key + anderer Request = Fehler
   • Header "Idempotent-Replayed: true"

3. DESIGN
   • PUT statt POST wo möglich
   • Client-generierte IDs
   • Absolute statt relative Änderungen
   • Versioning/ETags für Updates

4. FEHLERBEHANDLUNG
   • Retryable vs Non-retryable Errors
   • Exponential Backoff
   • Idempotent auch bei Fehlern
💡 Zusammenfassung: 1. Idempotency-Key Header für POST Requests
2. Client-generierte IDs als Alternative
3. DELETE immer 204, auch wenn nicht existiert
4. Absolute Werte statt Deltas bei Updates
5. Response cachen für Replay-Requests

Weitere Informationen

Enjix Beta

Enjyn AI Agent

Hallo 👋 Ich bin Enjix — wie kann ich dir helfen?
120