Banking-Backend in Laravel 13: die Architektur im Überblick
Laravel 13 erschien am 17. März 2026, verlangt mindestens PHP 8.3 und erhält Bugfixes bis Q3 2027 sowie Security-Fixes bis zum 17. März 2028 (Release Notes). Keine dieser Zahlen handelt von Banken. Sie bilden zusammen ein Fundament, das du in einem Audit verteidigen kannst.
Die Übung lautet: den Kern einer Bank, Konten, Transfers, KYC, auf Laravel 13 entwerfen und ehrlich markieren, wo das Framework die Last trägt und wo mein Code sie tragen muss. Der Rundgang beruht auf einem echten Banking-Backend, an dem ich gearbeitet habe, anonymisiert und vereinfacht, und nichts in diesem Text ist Compliance-Ratschlag. Der Anker ist real: sechs Monate Laravel 13 in meinem Multi-Tenant-SaaS, dessen Queues bereits Zahlungs-Webhooks befördern.
Warum interessiert sich eine Bank für Release-Termine?#
Eine Bank plant in Jahren, und Laravel legt den Kalender offen: Majors jährlich etwa um Q1, Bugfixes für 18 Monate, Security-Fixes für zwei Jahre, bei jedem Release. Das Security-Fenster von Laravel 11 schloss sich am 12. März 2026, und Laravel 12 erhält seit dem 13. August 2026 keine Bugfixes mehr. Laravel 13 läuft bis Q3 2027 und den 17. März 2028. Das ist in einer regulierten Codebasis ein Audit-Befund, der nur auf seinen Kalendereintrag wartet. Der 17. März 2028 gilt ab heute als Deadline, mit achtzehn Monaten Vorlauf und einem eintägigen Upgrade-Pfad, den mein erstes halbes Jahr auf dem Framework bereits getestet hat.
Die Mauern zwischen den Modulen#
Die Architektur ist ein modularer Monolith: ein Deployment, eine Datenbank, harte interne Grenzen. Laravel hilft dabei nicht. Das Framework hält dich nicht auf. Die Struktur-Dokumentation beschreibt einen einzelnen Anwendungsbaum ohne Modulsystem. Die Mauern sind reine Konvention: ein CI-Check auf Imports über Modulgrenzen hinweg und ein Service Provider pro Modul.
| Modul | Besitzt | Fasst nie an |
|---|---|---|
| Ledger | Konten, Salden, Journal-Einträge | User, Provider, Dokumente |
| Payments | Transfer-Intents, Idempotenz-Schlüssel, Provider-Clients | Saldo-Arithmetik |
| KYC | Dokumente, Review-Status, Screening-Ergebnisse | Geldbewegung |
| Identity | User, Credentials, Passkeys, Sessions | Journal-Zeilen |
| Notifications | Templates, Zustellung, Retries | Fachentscheidungen |
Module reden über Actions und Interfaces miteinander. Die Tabellen der anderen bleiben tabu. Eine Grenze, die sich nicht auf einer Seite ablesen lässt, übersteht die erste Deadline nicht.
Dünne Controller, laute Attributes#
Die HTTP-Schicht validiert und reicht weiter. Die Controller-Attributes von Laravel 13 machen die Sicherheitslage auf Klassenebene lesbar (Controller-Dokumentation):
#[Middleware(['auth', 'verified'])]
class TransferController
{
#[Authorize('create', [Transfer::class])]
public function store(StoreTransferRequest $request, InitiateTransfer $initiate)
{
return TransferResource::make(
$initiate->handle($request->user(), TransferData::from($request->validated()))
);
}
}Ein Form Request prüft die Eingaben, ein DTO friert das Ergebnis in typisierte Properties ein, und die Action empfängt ein Objekt, dessen Typen die Wahrheit sagen. Ein Array verlangt dagegen Keys, die du dir merken musst. Die Action läuft anschließend aus einem Job, einem Artisan-Command oder einem Test. Dicke Controller sind in einer Blog-App ein Code Smell. Eine Bank meldet sie als Befund.
Wie Geld sich genau einmal bewegt#
Jeder Transfer schreibt zwei Journal-Einträge, einen im Soll und einen im Haben. Das ist doppelte Buchführung. Der Saldo ergibt sich aus dem Ledger, nicht aus einer gespeicherten Meinung. Idempotenz heißt: der Client legt mit jedem Transfer einen Schlüssel bei, und das System bedient jeden Schlüssel genau einmal. Keines davon kommt aus dem Framework. Die Datenbank-Dokumentation gibt mir Transaktionen, die Buchführung ist meine:
DB::transaction(function () use ($data) {
$seen = IdempotencyKey::where('key', $data->idempotencyKey)
->lockForUpdate()
->first();
if ($seen) {
return $seen->transfer;
}
Entry::pairFor($data); // one debit, one credit
return Transfer::settle($data, IdempotencyKey::record($data->idempotencyKey));
});Die eigene Idempotenz-Maschinerie von Laravel liegt eine Ebene tiefer, beim Dispatch: ShouldBeUnique, das Attribut #[UniqueFor] und die Middleware WithoutOverlapping bauen auf atomaren Cache-Locks auf (Queues-Dokumentation). Keines dieser Mittel speichert einen Client-Schlüssel für einen API-Transfer. Diese Tabelle samt Row-Lock ist Anwendungscode. Mein SaaS betreibt dieselbe Bauform eine Stufe kleiner: byte-identische Webhook-Redeliveries sterben auf einem Hash-Journal und erreichen nie eine zweite Buchung.
Queue-Disziplin für Geld-Jobs#
Geld-Jobs bekommen das komplette Attribute-Set, weil hier die Fehlermodi zählen:
#[Tries(5)]
#[Backoff([30, 300, 900])]
#[Timeout(60)]
#[FailOnTimeout]
#[MaxExceptions(3)]
#[CountCrashesAsExceptions]
class SettleTransferJob implements ShouldQueue
{
}#[Tries] auf der Klasse schlägt das --tries-Flag des Workers, und #[Backoff] staffelt die Wiederholungen. Das Timeout-Paar macht ein hängendes Settlement zum Fehler, und das Exception-Limit begrenzt Versuche, die in einer Exception enden. Der neueste Zusatz stammt aus dem September: der Crash-Zähler lässt einen Versuch, dessen Worker per OOM stirbt, in diese Begrenzung zählen (Changelog, PR laravel/framework#61737). Das ist der Unterschied zwischen begrenztem Versagen und endlosem Retry.
Queue::route() ist neu in 13 und richtet sich nach Klasse, Interface, Trait oder Elternklasse:
Queue::route(SettleTransferJob::class, connection: 'redis', queue: 'payments');
Queue::route(RunsKycScreening::class, queue: 'kyc');
Queue::route(SendsCustomerMessage::class, queue: 'notifications');- payments: ein einzelner Worker, serialisiert hinter dem Ledger. Nichts Langsames teilt diese Spur.
- kyc: Screening-Calls bei Drittanbietern, minutenlang und gelegentlich fehlerhaft. Die Isolation schützt Payments vor Ausfällen der Anbieter.
- notifications: die günstige und laute Queue, die bei Überlast zuerst Last abwirft. Meine gequeueten Telegram-Alerts leben dort und mussten nie wissen, was ein Ledger ist.
Die Queue-Dokumentation empfiehlt diese Aufteilung, mit Workern, die Queues nach Priorität bedienen:
php artisan queue:work --queue=payments,kyc,defaultPasskeys mit Vorbehalt#
Die Übung trifft beim Login auf Entscheidungen von Anbietern. Die Standard-Starter-Kits liefern Fortify mit TOTP als zweitem Faktor, solide und ohne Glamour. Passkeys kommen über die WorkOS-AuthKit-Variante jedes Starter-Kits, in React, Svelte, Vue oder Livewire, angewählt mit einem Flag bei laravel new, kostenlos bis eine Million monatlich aktive Nutzer (Starter-Kits-Dokumentation). Teile der Release-Berichterstattung nannten das „passkey authentication“ in Laravel. Die Dokumentation schreibt WorkOS-Variante und hört dort auf, und diese Abhängigkeit von einem Anbieter sollte eine Bank mit offenen Augen wählen.
Passkeys bezeichnen hier die WorkOS-AuthKit-Variante des Starter-Kits. Das Framework selbst liefert nichts davon. Zertifizierungen von WorkOS habe ich gegen keinen Bankenregulator geprüft, die Compliance-Frage bleibt offen.
Login und KYC bleiben getrennt: Dokumente und Review-Status leben im KYC-Modul, und die Authentifizierung entscheidet, wer fragt.
Was du in dein Projekt übernimmst#
Die Bank ist das Gedankenexperiment, die Liste ist real:
- Zeichne die Modul-Tabelle vor der ersten Migration und lass CI sie durchsetzen. Später gezogene Mauern sind Dekoration.
- Schiebe jede Geld-Transaktion in eine Action hinter einem DTO, aufrufbar aus Jobs und Tests.
- Packe das Eintragspaar und den Idempotenz-Schlüssel in eine Transaktion und teste zuerst den doppelten Submit.
- Kopiere das komplette Attribute-Set auf jeden Job, der Geld berührt, den Crash-Zähler inklusive.
- Lege
Queue::route()in einen Provider pro Modul und gib Payments eine eigene Queue. - Lies die Support-Tabelle einmal pro Quartal und trag den 17. März 2028 in den Kalender ein, solange es Routine ist.
Nichts davon verlangt eine Banklizenz. Alles davon kostet ein, zwei Tage in einer Codebasis, die du selbst betreibst. Die Investition trägt sich, sobald der erste fehlgeschlagene Job meldet, woran er gestorben ist.