Vibe Coding: die versteckten Kosten KI gebauter Architekturen
Kurz gesagt
Niveau: Fortgeschritten
KI schreibt Code, der läuft. Sie trifft nicht die architektonischen Entscheidungen, nach denen niemand gefragt hat, und die entscheiden, ob ein System echte Kunden übersteht.
- Die Datenbank scheitert zuerst: N+1 Abfragen, fehlende Indizes, eine Datenbank aus Zufall und ein Schema, das nicht wachsen kann.
- Frameworks haben eine Decke: Streamlit, Gradio und Dash sind für Daten Apps und Demos, Flask und FastAPI überlassen dir die Struktur, Django und Rails legen sie für dich fest.
- Die Sprache zählt weniger als die Architektur, aber wähl sie für das Team und die Aufgabe.
- Raus in der richtigen Reihenfolge: zuerst Tests, dann an Ort und Stelle umbauen, Stück für Stück ersetzen, und nur als letzte Möglichkeit neu schreiben.
KI baut dir an einem Nachmittag einen funktionierenden Prototyp. Was sie nicht tut, solange niemand fragt, ist die Entscheidungen zu treffen, mit denen dieser Prototyp echten Traffic übersteht. Dieser Artikel zeigt, wo Systeme aus Vibe Coding zuerst brechen, wofür jedes verbreitete Framework wirklich gedacht ist, nach welchen Warnzeichen du in einer Codebasis suchst und welche drei Wege zurück es gibt.
Der Prototyp läuft, und genau das ist die Falle
Jemand hat eine Idee, öffnet ChatGPT oder Claude und fängt an zu bauen. Die KI schreibt den Code, der Prototyp läuft, die Gründerin ist begeistert. Sechs Monate später ächzt die Datenbank, Seiten brauchen Sekunden zum Laden, und eine Entwicklerin wird geholt, die sagt, dass große Teile neu gebaut werden müssen.
Der Name dafür ist Vibe Coding. Andrej Karpathy hat ihn im Februar 2025 geprägt, für eine Art zu bauen, bei der du dich "fully give in to the vibes" und "forget that the code even exists": jede Änderung annehmen, Fehlermeldungen zurück in den Chat kopieren, die Diffs nie lesen. Er hat klar gesagt, dass das zu "throwaway weekend projects" passt.1 Der Ärger beginnt, wenn das Wochenendprojekt zum Geschäft wird.
Die KI schreibt Code, der läuft. Was sie nicht tut, solange niemand danach fragt, ist die Entscheidungen zu treffen, mit denen ein System wachsen und echte Kunden überstehen kann. Dieser Artikel zeigt, wo per Vibe Coding entstandene Architekturen zuerst scheitern, wofür jede verbreitete Sprache und jedes Framework wirklich gedacht ist, und an welchen Warnzeichen du das früh erkennst.
KI schreibt Code, keine Architektur
Damit es klar ist: KI Assistenten fürs Programmieren sind nützlich. Ich nutze sie täglich. Aber Code schreiben und ein System entwerfen sind verschiedene Aufgaben. Code schreiben heißt, Logik in Syntax zu verwandeln, und darin ist KI gut. Ein System entwerfen heißt zu verstehen, wie die Daten fließen und wachsen, den Engpass zu sehen, bevor er kommt, ein Anliegen gegen ein anderes abzuwägen und zu wissen, was du noch nicht weißt. Ein Modell kennt dein Geschäft nicht und auch nicht deine Größenordnung. Es weiß nicht, dass deine "einfache Tabelle für Nutzer" in 18 Monaten 10 Millionen Zeilen haben wird. Es beantwortet die Anfrage, die vor ihm liegt.
Die Forschung sagt dasselbe von zwei Seiten. In einer randomisierten kontrollierten Studie, die METR 2025 durchgeführt hat, erledigten 16 erfahrene Open Source Entwickler 246 echte Aufgaben in Projekten, die sie gut kannten. Mit erlaubten KI Werkzeugen brauchten sie 19 % länger und glaubten hinterher trotzdem, die Werkzeuge hätten sie um 20 % schneller gemacht.2 Und als ein Forschungsteam GitHub Copilot 89 sicherheitsrelevante Szenarien vervollständigen ließ, waren rund 40 % der 1.689 erzeugten Programme verwundbar.3 Geschwindigkeit, die du spürst, ist nicht dasselbe wie Qualität, auf die du dich verlassen kannst.
Ein Beispiel: Eine Gründerin bittet um "eine Seite, die alle Bestellungen mit dem Namen des Kunden auflistet". Mit 20 Testbestellungen läuft das, und niemand fragt, wie es sich bei 20.000 verhält. Diese Frage ist die Architektur, und der nächste Abschnitt beantwortet sie.
In der Datenbank scheitert Vibe Coding zuerst
In der Datenbank häufen sich architektonische Schulden am schnellsten an. Eine Entscheidung zum Schema aus der ersten Woche wird im zweiten Jahr zu einer Migration über Millionen Zeilen.
Das N+1 Problem
Das ist der häufigste Leistungskiller in Anwendungen aus Vibe Coding. Er entsteht, wenn Code eine Liste holt und dann für jeden Eintrag der Liste noch eine weitere Datenbankabfrage macht.
# Die Fassung aus Vibe Coding (N+1 Abfragen)
orders = Order.objects.all() # 1 Abfrage
for order in orders:
print(order.customer.name) # 1 weitere Abfrage pro Bestellung
# 1.000 Bestellungen = 1.001 Datenbankabfragen
# Die korrigierte Fassung (eine Abfrage mit Join)
orders = Order.objects.select_related("customer")
for order in orders:
print(order.customer.name) # keine zusätzlichen Abfragen
Jede Zeile ist für sich richtig, deshalb rutscht der Fehler durch. Nichts an order.customer sieht nach einem Datenbankaufruf aus, aber in Django ist es einer, wie in den meisten ORMs (den objektrelationalen Mappern, die Zeilen in Objekte verwandeln).
Die Zahl der Abfragen wächst mit der Liste:
Dabei ist die Zahl der Abfragen, die die Seite macht, wenn jeder Kunde einzeln geladen wird, und die Zahl der Bestellungen auf der Seite. Jede weitere Bestellung bringt eine weitere Abfrage, die Grenzkosten einer Bestellung sinken also nie. Mit einem Join bleibt bei einer oder zwei, egal wie groß ist. Grundlagen: Grenzwert und Durchschnitt
Die Zeit, die die Seite auf die Datenbank wartet, ist dann:
Dabei ist die gesamte Wartezeit, die Zahl der Abfragen und die durchschnittliche Zeit für eine Abfrage, einschließlich des Wegs zum Datenbankserver und zurück. Grundlagen: Verhältnisse und Raten
Beispiel: eine Bestellliste bei einem Leipziger Teeladen
Teekontor Lindenau ist ein erfundener Onlineshop für Tee in Leipzig, unter teekontorlindenau.example. Seine Verwaltungsseite listet die letzten 400 Bestellungen mit dem Namen jedes Kunden. Nimm für das Beispiel an, dass eine kleine Abfrage samt Hin und Rückweg 2 Millisekunden dauert.
- Die Seite aus Vibe Coding macht Abfragen.
- Die Wartezeit ist Millisekunden. Langsam, aber erträglich, also fällt es niemandem auf.
- Im Dezember exportiert die Inhaberin 10.000 Bestellungen. Das sind Millisekunden, 20 Sekunden Starren auf ein Ladesymbol.
- Die korrigierte Seite macht eine Abfrage mit Join. Selbst wenn die 10 Millisekunden dauert, wartet die Seite 10 Millisekunden, bei 400 Bestellungen wie bei 10.000.
Die waagrechte Achse zeigt die Zahl der Bestellungen auf der Seite, die senkrechte die Abfragen, die sie schickt. Einzeln geladen steigt die Linie mit jeder Bestellung, der Join bleibt flach bei eins. Zwei Seiten, die in der Entwicklung gleich aussehen, verhalten sich bei Last völlig verschieden.
Fehlende Indizes
Ein Index ist für eine Tabelle, was das Register hinten in einem Buch für dessen Seiten ist: ein sortiertes Verzeichnis, das dich direkt an die richtige Stelle bringt. Ohne Index liest die Datenbank jede Zeile, um die gesuchten zu finden, ein vollständiger Tabellenscan.
-- Eine Abfrage, die viele Male pro Sekunde läuft
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
-- Der Index, der sie bedient
CREATE INDEX index_orders_on_user_id_and_status ON orders (user_id, status);
Spalten, nach denen du oft filterst, verknüpfst oder sortierst, brauchen meist einen, und die Reihenfolge der Spalten zählt: Die Dokumentation von PostgreSQL erklärt, dass ein Index über mehrere Spalten am besten wirkt, wenn die Abfrage seine führenden (am weitesten links stehenden) Spalten einschränkt.4 Nichts in einer Bitte wie "leg eine Tabelle für Bestellungen an" fragt nach einem Index, also bleiben die Kosten unsichtbar, bis die Tabelle groß ist.
Der Unterschied in der Arbeit ist der Unterschied zwischen dem Lesen jeder Zeile und dem Abstieg durch einen sortierten Baum:
Dabei ist die Zahl der Zeilen, die ein vollständiger Scan liest, die Zahl der Zeilen in der Tabelle, und ungefähr die Zahl der Schritte, die eine sortierte Suche braucht, und die wächst mit dem Logarithmus von . Echte Datenbankindizes verzweigen sich bei jedem Schritt viel breiter als in zwei Äste, sie brauchen also noch weniger Schritte. Grundlagen: Logarithmen
Beispiel: Buchungen in Köln
Sportplan Ehrenfeld ist eine erfundene Buchungsplattform für Fitnessstudios in Köln, unter sportplanehrenfeld.example. Ihre Tabelle mit Buchungen hat 12 Millionen Zeilen, und die Seite "meine Buchungen" filtert nach Mitglied und Status.
- Ohne Index liest die Datenbank bei jedem Seitenaufruf alle Zeilen.
- Mit einem Index auf (Mitglied, Status) braucht eine sortierte Suche rund Schritte, also etwa 24.
- Bei 50 Seitenaufrufen pro Sekunde muss die Datenbank in der Fassung mit Scan jede Sekunde 600 Millionen Zeilen lesen. Das ist keine langsame Seite, das ist ein Ausfall.
Die falsche Datenbank für das Zugriffsmuster
Vibe Coding beginnt meist mit SQLite oder PostgreSQL, weil die Tutorials und das Modell danach greifen. Beide sind für viele Aufgaben eine gute Wahl. Die Frage ist, ob die Wahl für dein Zugriffsmuster getroffen wurde oder aus Zufall.
| Zugriffsmuster | Passt meist gut | Oft ein Fehler |
|---|---|---|
| Relationale Daten mit vielen Joins | PostgreSQL, MySQL | Ein Dokumentspeicher, dessen Joins im Code nachgebaut werden |
| Dokumente mit stark wechselnder Form | MongoDB, oder PostgreSQL mit bewusst eingesetztem JSONB | Dutzende Spalten, die leer bleiben dürfen |
| Sehr viele Schreibvorgänge, Zeitreihen | TimescaleDB (eine Erweiterung für PostgreSQL), ClickHouse | Einfache Tabellen ohne Partitionierung und ohne Plan für die Aufbewahrung |
| Volltextsuche | Volltextsuche in PostgreSQL, Elasticsearch, Meilisearch | LIKE '%term%' auf einer großen Tabelle |
| Eine einzelne kleine App auf einem Server | SQLite | Ein Datenbankcluster, den noch niemand braucht |
SQLite verdient ein faires Wort. Seine Dokumentation sagt, es passe für die meisten Websites mit wenig bis mittlerem Traffic, mit weniger als 100.000 Aufrufen am Tag als vorsichtigem Richtwert, und seine echte Grenze sei ein einziger Schreibender zur selben Zeit.5 Rails 8 lässt darauf sogar Jobs, Caching und WebSockets laufen.6 Der Fehler ist nicht SQLite, sondern nie zu fragen, wie viele gleichzeitig schreiben werden.
Ich habe Projekte aus Vibe Coding gesehen, die Zeitreihen in PostgreSQL als neue Zeile pro Messpunkt speicherten, ohne Plan für das Volumen, und sich dann wunderten, warum Abfragen 30 Sekunden dauern. Ich habe Projekte gesehen, die stark relationale Daten in MongoDB speicherten und dann monatelang mit $lookup Aggregationen kämpften, die ein SQL Join in Millisekunden beantworten würde.
Beispiel: Zählerstände in Hamburg
Hafenzähler ist ein erfundenes Start up für Energiemonitoring in Hamburg, unter hafenzaehler.example, mit 2.000 Sensoren, die jede Minute einen Messwert schicken. Das Jahresvolumen ist:
Dabei ist die Zahl der Zeilen pro Jahr, die Zahl der Sensoren, die Messwerte pro Sensor und Tag und die Zahl der Tage. Es ist eine Rate, über die Zeit hochgerechnet. Grundlagen: Verhältnisse und Raten
- Messwerte pro Tag: , weil ein Tag 1.440 Minuten hat.
- Messwerte pro Jahr: , gut eine Milliarde Zeilen.
- Bei diesem Volumen brauchst du eine Partitionierung nach Zeit, stündliche Verdichtungen und eine Aufbewahrungsfrist. Nichts davon kommt in einem Prompt vor, der "speichere die Messwerte" sagt.
Ein Schema, das nicht wachsen kann
Schemas aus Vibe Coding werden für die Anforderung von heute gebaut. Nimm Adressen: eine Spalte address in users, dann eine Rechnungsadresse, dann eine Prüfung, dann Geocoding. Die Antwort aus Vibe Coding fügt Spalten hinzu. Die strukturierte Antwort ist eine eigene Tabelle addresses, mit dem Nutzer verknüpft, ab dem ersten Tag.
-- Vibe Coding (Woche 1)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address TEXT
);
-- Vibe Coding (Woche 52, viele Migrationen später)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
address_line_1 TEXT,
address_line_2 TEXT,
address_city TEXT,
address_postcode TEXT,
billing_address_line_1 TEXT,
billing_address_line_2 TEXT
-- ... und jedes Quartal mehr
);
-- Strukturiert (Woche 1)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
kind VARCHAR(20) NOT NULL, -- 'delivery', 'billing'
line_1 TEXT,
line_2 TEXT,
city TEXT,
postcode TEXT,
country_code CHAR(2),
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8)
);
Beispiel: die zweite Adresse eines Blumenladens
Blumen Haidhausen ist ein erfundener Blumenladen in München, unter blumenhaidhausen.example. Kundinnen wollen Blumen an eine Freundin schicken und die Rechnung nach Hause bekommen, und Firmenkunden wollen drei Büroadressen hinterlegen.
- Mit Adressspalten in
usersheißt jede weitere Adresse eine Migration, ein geändertes Formular und neuer Code überall, wo Adressen gelesen werden. - Mit einer Tabelle
addressesist eine zweite oder zehnte Adresse einfach eine weitere Zeile.
Wähl die Sprache für das Team und die Aufgabe
Die Sprache bestimmt, wen du einstellen kannst, welche Bibliotheken du hast, wo deine Leistungsgrenze liegt und wie schwer das Deployment wird. Sie zählt weniger als die Architektur, aber die falsche bringt bei jedem Schritt Reibung.
Python, der Standard in den meisten KI Sitzungen
Python hat ein riesiges Ökosystem, vor allem für Data Science, und ist leicht zu lernen, deshalb beginnen so viele Projekte aus Vibe Coding damit. Seine Schwächen werden oft übertrieben. Der Global Interpreter Lock (GIL) hat lange verhindert, dass Python Threads Python Code parallel auf mehreren Kernen ausführen, seit Python 3.13 gibt es aber einen optionalen Build ohne GIL (free threaded).7 Dynamische Typisierung lässt Fehler durch, die ein Compiler finden würde, außer du ergänzt Type Hints und ein Prüfwerkzeug.
Nimm es für Datenpipelines, Machine Learning, als Verbindung zwischen Systemen und für Teams, die es kennen. Überleg zweimal bei Diensten mit sehr hohem Durchsatz.
JavaScript und TypeScript, die Versuchung des Full Stack
Eine Sprache im Browser und auf dem Server ist verlockend, TypeScript bringt die Typsicherheit, die reinem JavaScript fehlt, und die Event Loop kommt gut damit zurecht, auf das Netzwerk zu warten. Der Preis ist eine kleine Standardbibliothek, fast alles ist also eine Abhängigkeit. Nimm es fürs Frontend (da gibt es keine Wahl), für Funktionen in Echtzeit und für Teams mit Schwerpunkt im Frontend.
Ruby, gebaut für Produktivität
Rails ist für komplexe Geschäftslogik sehr produktiv, seine Konventionen nehmen dir eine ganze Klasse von Entscheidungen ab, und die Kultur des Testens ist stark. Ruby ist langsamer als kompilierte Sprachen, es gibt weniger Entwickler, und zu viel Metaprogrammierung wird schwer nachvollziehbar. Es ist meine eigene Wahl für eCommerce auf Solidus und für SaaS Produkte, und die Gründe stehen in warum ich Next.js und Ruby on Rails nutze.
Go, pragmatisch bei Last
Go ist kompiliert, schnell, hat Nebenläufigkeit über Goroutines eingebaut und ist bewusst einfach. Generics kamen erst 2022 mit Go 1.18,8 und die explizite Fehlerbehandlung macht den Code wortreich. Nimm es für APIs mit hohem Durchsatz, Netzwerkdienste und Infrastrukturwerkzeuge.
Elixir, der Spezialist für Nebenläufigkeit
Elixir läuft auf der virtuellen Maschine von Erlang, die Nebenläufigkeit, Verteilung und Fehlertoleranz eingebaut hat und in großen Telekommunikationssystemen bei Ericsson läuft.9 Das Ökosystem ist kleiner. Nimm es für Funktionen in Echtzeit und sehr viele gleichzeitige Verbindungen.
Rust, wenn die Leistung nicht nachgeben darf
Rust bringt eine Leistung nah an C, mit Speichersicherheit und ohne Garbage Collector. Es ist mühsam zu lernen und mehr, als die meisten Webanwendungen brauchen. Nimm es für Systemprogrammierung und Dienste, bei denen die Leistung entscheidet.
Beispiel: eine Sprache für eine Leipziger Recruitingplattform
Eine erfundene Recruitingplattform in Leipzig hat zwei Python Entwickler und braucht Stellenanzeigen, Bewerbungen, Konten für Arbeitgeber und Rechnungen für ein paar Tausend Nutzer.
- Gewöhnlicher Webtraffic mit vielen Geschäftsregeln: Rohe Geschwindigkeit ist nicht die Grenze.
- Das Wissen des Teams ist es, also ist Python vernünftig.
- Die eigentliche Entscheidung ist das Framework: eines mit ORM, Migrationen, Authentifizierung und eingebauter Verwaltungsoberfläche. Das spricht für Django, nicht für eine Bibliothek für Dashboards oder ein nacktes API Framework.
Frameworks: wofür jedes gedacht ist und wo es bricht
Das sind die Frameworks, nach denen ich Projekte aus Vibe Coding greifen sehe. Jedes ist für etwas gut. Das Problem ist, es für etwas anderes zu nutzen.
Streamlit: eine Daten App, die zum Produkt wurde
Streamlit macht aus einem Python Skript mit sehr wenig Code eine Weboberfläche, deshalb erzeugen KI Werkzeuge es so gern. Der Haken ist das Ausführungsmodell. Jedes Mal, wenn jemand ein Widget ändert, führt Streamlit das Skript von oben bis unten neu aus. Die eigene Dokumentation sagt, lang laufende Funktionen liefen dann "again and again", und bietet Caching als Lösung an, wobei zwischengespeicherte Werte standardmäßig für alle Nutzer geteilt werden.10 Ein einziger Serverprozess rechnet für alle Nutzer, er muss also für die gleichzeitige Last bemessen sein.11
import streamlit as st
import pandas as pd
@st.cache_data(ttl=3600) # ohne diese Zeile läuft die Abfrage bei jedem Klick neu
def load_sales():
return pd.read_sql("SELECT * FROM sales", connection)
df = load_sales()
st.dataframe(df)
Manche alte Kritik gilt nicht mehr: Streamlit hat Apps mit mehreren Seiten über st.navigation12 und eine Anmeldung über einen OpenID Connect Anbieter mit st.login.13 Ein Anwendungsframework ist es trotzdem nicht. Es gibt kein ORM, keine Migrationen, keine Hintergrundjobs, und Logik, Datenzugriff und Darstellung teilen sich ein Skript.
Beispiel: ein Dashboard in Köln
Ein erfundener Großhändler in Köln baut sein Verkaufsdashboard in Streamlit. Die Hauptabfrage dauert 30 Sekunden. Zwanzig Mitarbeitende nutzen das Dashboard, und jede Person ändert etwa 15 Mal am Tag einen Filter.
Die tägliche Datenbankzeit ist:
Dabei ist die Datenbankzeit pro Tag in Sekunden, die Zahl der Nutzer, die Interaktionen pro Nutzer und Tag und die Sekunden pro Abfrage. Grundlagen: Verhältnisse und Raten
- Ohne Caching: Sekunden, zweieinhalb Stunden, in denen die Datenbank jeden Tag dieselbe Arbeit macht.
- Mit
st.cache_data, einmal pro Stunde aktualisiert, an einem Arbeitstag von acht Stunden: Sekunden. - Die Lösung ist eine Zeile, wenn die Person, die es ausliefert, weiß, dass es das Modell des Neuausführens gibt.
Gradio: gebaut für Demos
Gradio verpackt eine Python Funktion, typischerweise ein Machine Learning Modell, in eine Weboberfläche. Es bietet inzwischen eine Passwortoption bei launch() und eine Anmeldung mit "Sign in with Hugging Face", aber der eigene Leitfaden ist deutlich, was die Grenzen angeht: Die Passwortoption sei "a basic layer of access control", ohne Mehrfaktorauthentifizierung, Ratenbegrenzung oder Sperre.14 Konten, Zahlungen oder gespeicherte Ergebnisse im Sinn einer Anwendung gibt es nicht.
Ein Beispiel: Ein erfundenes Start up aus Dresden stellt drei Modelle hinter Gradio und nennt das eine Plattform. Als Kunden nach Konten, Rechnungen und einem Verlauf ihrer Ergebnisse fragen, muss all das von Grund auf um die Modelle herum gebaut werden. Gradio hat seinen Job gemacht, eine Demo. Niemand hat entschieden, wann aus der Demo ein Produkt werden soll.
Plotly Dash: Dashboards, keine Anwendungen
Dash baut analytische Web Apps in Python auf React Komponenten, mit einem Callback Modell, das strukturierter ist als Streamlit. Komplexe Apps können zu einem Geflecht aus Callbacks werden, das schwer nachzuverfolgen ist:
@app.callback(
Output("revenue-graph", "figure"),
Output("orders-graph", "figure"),
Output("orders-table", "data"),
Input("region-dropdown", "value"),
Input("channel-dropdown", "value"),
Input("date-range", "start_date"),
Input("date-range", "end_date"),
State("filters-store", "data"),
)
def update_everything(region, channel, start, end, filters):
# Welche Ausgabe hat sich geändert? Alle, jedes Mal.
...
Die Dokumentation von Dash warnt, dass die meisten Webserver nach 30 Sekunden abbrechen und lang laufende Callbacks jeden Worker blockieren können, deshalb gibt es Background Callbacks.15 Ein gutes internes Dashboard, aber Konten und Zahlungen baust du selbst.
Ein Beispiel: Ändere eine der fünf Eingaben oben, und alle drei Ausgaben werden neu berechnet. Braucht jede eine Abfrage von zwei Sekunden, kostet jeder Klick sechs Sekunden, auch wenn nur die Tabelle neu sein musste. Das in drei Callbacks aufzuteilen, die jeweils nur auf das hören, was sie nutzen, ist eine architektonische Entscheidung, keine Korrektur der Syntax.
Flask: absichtlich klein
Flask gibt dir Routing und wenig sonst, und das mit Absicht. ORM (SQLAlchemy), Migrationen (Alembic), Login, Formulare und eine Verwaltungsoberfläche ergänzt du selbst. In einem Projekt aus Vibe Coding ist genau diese Freiheit das Problem, weil jeder Prompt eine andere Wahl trifft.
Zwei verbreitete Kritikpunkte brauchen eine Korrektur. Flask unterstützt seit Version 2.0 async Views, auch wenn als WSGI Anwendung jede Anfrage weiter einen Worker belegt, und die Dokumentation verweist für Arbeit, die von Grund auf asynchron ist, auf Quart.16 Und die global wirkenden Objekte (request, g, current_app) sind Context Locals, mit denen ein Test Client gut zurechtkommt. Flask zu testen ist nicht schwer. Ein Flask Projekt ohne Struktur zu testen schon.
Ein Beispiel: Ein erfundenes Start up aus Hamburg baut sein Produkt über 18 Monate voller Prompts in Flask und hat am Ende vier Arten von Datenbankzugriff und zwei Mechanismen für den Login, einen pro erzeugter Funktion. Das Framework hat es erlaubt. Ein Mensch, der am ersten Tag die Struktur festgelegt hätte, nicht.
Django: alles inklusive
Django bringt ORM, Migrationen, Authentifizierung, eine Verwaltungsoberfläche und Formulare mit. Entscheidungen sind für dich getroffen, und genau das fehlt einem Projekt aus Vibe Coding. Instagram gehört zu den Websites, die das Django Projekt als damit gebaut aufführt.17
Das ORM ist bequem, bis du Leistung brauchst. Das verschachtelte N+1 Problem ist der klassische Fall:
for order in Order.objects.all(): # 1 Abfrage
print(order.customer.name) # 1 Abfrage pro Bestellung
for item in order.items.all(): # 1 Abfrage pro Bestellung
print(item.product.name) # 1 Abfrage pro Position
Bei Async hat sich das Bild weiterentwickelt: Django unterstützt asynchrone Views unter ASGI und, mit einigen Ausnahmen, asynchrone Abfragen im ORM, auch wenn die Dokumentation sagt, an der asynchronen Unterstützung des ORM werde noch gearbeitet.18
Beispiel: eine Bestellseite in Django
Nimm 50 Bestellungen mit je drei Positionen. Die Schleife oben macht:
Dabei ist die Zahl der Abfragen, die Zahl der Bestellungen und die Positionen pro Bestellung: eine Abfrage für die Bestellungen, eine pro Bestellung für den Kunden, eine pro Bestellung für die Positionen und eine pro Position für das Produkt. Grundlagen: Grenzwert und Durchschnitt
- Abfragen.
- Umgeschrieben zu
Order.objects.select_related("customer").prefetch_related("items__product")macht sie drei: Bestellungen per Join mit den Kunden, dann alle Positionen, dann alle Produkte. - Bei 2 Millisekunden pro Abfrage sind das 502 Millisekunden gegen 6, für dieselbe Seite.
FastAPI: ein API Framework, das alles sein soll
FastAPI ist ein modernes Python Framework für APIs, mit Type Hints, automatischer OpenAPI Dokumentation und Unterstützung für Async. Darin ist es hervorragend, und es ist kein Full Stack Framework: Es gibt kein ORM, keine Migrationen, keine Verwaltungsoberfläche und keine Nutzerverwaltung, also setzt du sie selbst zusammen.
Der häufigste Fehler betrifft Async, und die ursprüngliche Fassung dieses Artikels hatte die Lösung falsch. FastAPI ist nicht "standardmäßig asynchron". Deklarierst du einen Endpunkt mit einfachem def, führt FastAPI ihn in einem Thread Pool aus, ein blockierender Datenbankaufruf friert den Server also nicht ein. Deklarierst du ihn mit async def und rufst darin blockierenden Code auf, blockierst du die Event Loop für alle.19
# Blockiert die Event Loop: async def mit einem synchronen Datenbankaufruf
@app.get("/users")
async def list_users():
return db.query(User).all()
# In Ordnung: einfaches def, FastAPI führt es in seinem Thread Pool aus
@app.get("/users")
def list_users():
return db.query(User).all()
Ein Beispiel: Zehn Anfragen kommen gleichzeitig, und jede macht in async def eine blockierende Abfrage von 200 Millisekunden. Sie laufen nacheinander, der letzte Aufrufer wartet also Millisekunden. Mit einfachem def lässt der Thread Pool sie nebeneinander laufen, und jeder wartet rund 200 Millisekunden. Ein Schlüsselwort, ein zehnfacher Unterschied bei der längsten Wartezeit.
Die architektonische Falle ist FastAPI plus ein separates React Frontend, in zwei Hälften erzeugt: die Validierung zweimal geschrieben, mehrere Verfahren zur Authentifizierung, zwei Repositories, die zusammen deployt werden müssen.
Reflex: jung und vielversprechend
Mit Reflex schreibst du eine Full Stack Web App in Python. Unter der Haube kompiliert es die Oberfläche zu einem React Frontend, das an ein FastAPI Backend angebunden ist, und gewöhnliche State Handler laufen auf dem Server, wobei Events und Änderungen am State über WebSockets wandern.20
Das ist der Zielkonflikt. Jede Änderung am State geht zum Server und zurück, gut für interne Werkzeuge, langsam für sehr interaktive Oberflächen. Das Ökosystem ist jung, und wenn etwas zwischen dem Python und dem kompilierten React bricht, musst du beides verstehen.
Ein Beispiel: Ein erfundenes Werkzeug aus Leipzig aktualisiert den State bei jedem Tastendruck in einem Suchfeld. Nimm einen Hin und Rückweg von 120 Millisekunden an: Eine Suche mit neun Buchstaben schickt neun Events, und die Ergebnisse hinken dem Tippen hinterher. Die Eingabe zu entprellen oder diesen State im Browser zu halten, löst das, wenn jemand weiß, danach zu fragen.
Rapid Development Frameworks, die die Last tragen
Hier muss schnell sein nicht heißen, später neu zu bauen. Über das, was Rapid Development Frameworks wirklich sind, habe ich gesondert geschrieben. Kurz gesagt: Konventionen, die Tausende Projekte in Produktion festgelegt haben, sodass niemand sie in einem Prompt erfindet.
Ruby on Rails
Rails trägt in Produktion ernsthaft Last: Der Kern von Shopify ist ein modularer Monolith auf Rails,21 und GitHub hat über das Upgrade seiner eigenen großen Rails Anwendung geschrieben.22 Die Stolperfallen sind bekannt, die Lösungen auch.
Konvention heißt, dass jede Rails App gleich aufgebaut ist, eine neue Entwicklerin oder ein Reviewer findet die Dinge also dort, wo sie sie erwarten. Rails hat sogar einen eingebauten Schutz gegen N+1 Abfragen, Strict Loading, das einen Fehler wirft, sobald Code eine Assoziation verzögert lädt, statt sie vorab zu laden.23 (Das beliebte Gem Bullet macht etwas Ähnliches, ist aber eine Ergänzung und nicht Teil von Rails.)
# config/environments/development.rb
config.active_record.strict_loading_by_default = true
# Jetzt wirft das in der Entwicklung einen Fehler, statt still N+1 Abfragen zu machen
Order.all.each { |order| puts order.customer.name }
# So will es das haben
Order.includes(:customer).each { |order| puts order.customer.name }
Rails 8 hat mehr vom Stack ins Framework geholt: Solid Queue für Jobs, Solid Cache und Solid Cable auf der Datenbank, und einen Generator für die Authentifizierung.6 Weniger einzelne Entscheidungen, die ein Prompt falsch treffen kann.
Wo Rails sich schwertut: bei rechenintensiver Arbeit, bei sehr niedrigen Anforderungen an die Latenz und bei Teams, die seinen Konventionen nicht folgen wollen. Der Weg zur Skalierung ist gut ausgetreten: Caching, Read Replicas, Background Jobs und ein eigener Dienst nur für die heißesten Pfade.
Ein Beispiel: Ein erfundener Kölner Shop für Wohnaccessoires schaltet Strict Loading in der Entwicklung ein. Wenn eine erzeugte Verwaltungsseite über 400 Bestellungen läuft und order.customer liest, wirft sie auf dem Bildschirm der Entwicklerin einen Fehler, statt so live zu gehen. includes(:customer) bringt die Seite von 401 Abfragen auf zwei (die Bestellungen, dann die Kunden).
Phoenix, für viele Verbindungen auf einmal
Phoenix bringt Produktivität im Stil von Rails auf die virtuelle Maschine von Erlang. Ihre Prozesse sind leichtgewichtig, ein Server kann also enorm viele davon laufen lassen, und fällt einer aus, reißt er die anderen nicht mit. 2012 meldete WhatsApp mehr als 2 Millionen TCP Verbindungen auf einem einzigen Erlang Server,24 und 2015 erreichte das Team von Phoenix in einem Benchmark 2 Millionen WebSocket Verbindungen auf einer Maschine.25 Mit LiveView baust du interaktive Seiten mit sehr wenig JavaScript im Frontend.
Weniger Entwickler kennen es. Ein Beispiel: Ein erfundenes Start up für Veranstaltungen in München braucht Saalpläne in Echtzeit, auf die Tausende Menschen gleichzeitig schauen, während Plätze verkauft werden. Viele offene Verbindungen, von denen jede wenig tut, sind genau die Form, mit der Phoenix gut umgeht.
Laravel, auf PHP
Laravel ist ein Full Stack Framework für PHP mit guter Dokumentation, Starter Kits für Authentifizierung, Queues und Broadcasting, und PHP Hosting gibt es überall. Modernes PHP ist besser als sein Ruf: PHP 8.0 brachte Union Types, Attribute, den Match Ausdruck und einen JIT Compiler.26
Wo Laravel sich schwertut, tut sich meist PHP schwer, und darum geht es im nächsten Abschnitt.
Warum PHP besondere Sorgfalt braucht
PHP hat einen eigenen Artikel. Es bringt dich sehr schnell zu "es läuft", und genau das macht ein PHP Projekt aus Vibe Coding gefährlich: Es läuft lange, bevor es richtig ist.
Die Sprache selbst
Die Standardbibliothek ist uneinheitlich, die Reihenfolge der Argumente ist also ein Gedächtnistest:
strpos($haystack, $needle); // haystack zuerst
strstr($haystack, $needle); // haystack zuerst
in_array($needle, $haystack); // needle zuerst
array_search($needle, $haystack); // needle zuerst
Der lose Vergleich mit == wandelt Typen um, bevor er vergleicht. PHP 8 hat das deutlich vernünftiger gemacht: Seit der Änderung "saner string to number comparisons" sind 0 == "" und 0 == "foo" falsch, wo PHP 7 wahr sagte.27 Eine Falle bleibt, und es ist die gefährliche: Sind beide Seiten numerische Strings, vergleicht PHP sie als Zahlen.28
// PHP 8
0 == "0" // true
0 == "" // false (vor PHP 8 true)
null == false // true
// Auch in PHP 8 noch true: beides numerische Strings in wissenschaftlicher Schreibweise, beide gleich 0
"0e1234" == "0e5678" // true
// Also nie Geheimnisse mit == vergleichen
if (hash_equals($stored_hash, $computed_hash)) {
// ...
}
Beispiel: eine Passwortprüfung, die jeden hereinlässt
Eine erfundene Mitgliederseite aus Hamburg speichert ungesalzene MD5 Hashes und prüft Logins mit if ($stored == md5($input)). Der gespeicherte Hash eines Mitglieds besteht zufällig aus 0e und danach nur Ziffern. Jede Eingabe, deren MD5 Hash dieselbe Form hat, passt jetzt, weil PHP beides als Null in wissenschaftlicher Schreibweise liest. Der Angreifer braucht kein Passwort, nur so eine Eingabe. Die Lösungen: password_hash() und password_verify() für Passwörter, hash_equals() für den Vergleich anderer Geheimnisse und === überall sonst.
Das Ökosystem
Composer, der Paketmanager von PHP, veröffentlichte 2013 seine erste Alpha und 2016 die Version 1.0,29 rund zwei Jahrzehnte, nachdem Rasmus Lerdorf PHP 1994 geschaffen hatte.30 Viel älteres PHP wurde mit kopiertem Fremdcode und händischen include Zeilen gebaut, und es läuft noch.
Klassisches PHP heißt eine Anfrage, ein kurzlebiger Prozess. PHP 8.1 brachte Fibers als Baustein für Nebenläufigkeit,31 und Bibliotheken wie ReactPHP füllen Lücken, aber lang laufende Arbeit und WebSockets heißen meist, über die klassische Anfrage hinauszugreifen.
WordPress läuft laut W3Techs auf 39,4 % aller Websites.32 Seine Sicherheitsbilanz wird oft PHP angelastet, deshalb lohnt sich Genauigkeit: Die Zahlen von Patchstack für 2023 verzeichneten 5.948 neue Schwachstellen im Ökosystem von WordPress, 97 % davon in Plugins und 0,2 % im Kern von WordPress.33 Das Risiko ist der Wildwuchs an Plugins, und ein Plugin aus Vibe Coding ist Wildwuchs, den niemand versteht.
Wann PHP die richtige Wahl ist
Ich sage nicht, nimm nie PHP. Nimm es, wenn du WordPress, Drupal oder Magento erweiterst, wenn dein Team modernes PHP gut kennt, wenn du Laravel nach heutigem Stand einsetzt oder wenn das Hosting es verlangt. Denk gut nach, bevor du es für ein neues Projekt wählst, bei dem du frei entscheiden kannst, für Funktionen in Echtzeit oder dort, wo rohe Leistung zählt.
Die Warnzeichen als Checkliste
Bevor du baust, oder wenn du eine Codebasis übernimmst, schau nach diesen Punkten.
Datenbank
- Keine Fremdschlüssel definiert
- Keine Indizes außer auf den Primärschlüsseln
- Jede Spalte
VARCHAR(255)oderTEXT - Änderungen am Schema von Hand, ohne Migrationen
- SQLite mit mehreren Servern oder vielen gleichzeitig Schreibenden
Architektur
- Die ganze Logik in den Route Handlern, ohne Service Schicht
- Keine Verarbeitung im Hintergrund, alles passiert in der Anfrage
- Geheimnisse im Code statt in der Umgebung
- Dateien mit mehr als 1.000 Zeilen
- Fehler, die still verschluckt werden
Framework
- Streamlit oder Gradio bedienen zahlende Kunden
- Flask ohne festgelegte Struktur für den Datenzugriff
- FastAPI plus ein Frontend, zusammengeklebt mit doppelter Validierung
- Eine Struktur, kopiert aus einem Tutorial "in zehn Minuten gebaut"
Tests
- Keine Tests
- Tests, die nur auf dem Rechner einer Entwicklerin bestehen
- "Testen" heißt, sich durch die Oberfläche zu klicken
- Keine Continuous Integration
Deployment
- Deployment per FTP, oder per SSH und
git pull - Keine Staging Umgebung
- Kein Weg zurück (Rollback)
Beispiel: die Checkliste lesen
Ein erfundenes Start up aus Dresden prüft sein Produkt gegen die 21 Kästchen und hakt sieben ab: keine Fremdschlüssel, keine zusätzlichen Indizes, Logik in den Route Handlern, verschluckte Fehler, keine Tests, keine Continuous Integration, kein Rollback. Die Zahl zählt weniger als das Muster. Drei der sieben liegen bei Tests und Deployment, also würde nichts eine Regression bemerken und nichts sie rückgängig machen können. Dort beginnt die Arbeit, weil jede spätere Änderung davon abhängt.
Wie du dich von Vibe Coding erholst
Erkennst du dein Projekt wieder, hast du drei Möglichkeiten, und die Wahl zwischen ihnen ist selbst eine architektonische Entscheidung.
Der Ablauf geht von oben nach unten. Jeder Weg beginnt mit Tests, denn ohne sie kannst du nicht sagen, ob eine Änderung das Verhalten erhalten hat. Die erste Frage entscheidet, ob das Fundament bleibt, die zweite, ob sich das System Stück für Stück ersetzen lässt. Das Neuschreiben steht mit Absicht am Ende des letzten Zweigs.
An Ort und Stelle umbauen
Das funktioniert, wenn die Codebasis klein genug und das Framework nicht grundsätzlich falsch ist. Ergänze Tests für das, was da ist, zieh die Geschäftslogik in Services, korrigiere das Schema Schritt für Schritt und verbessere ein Teilsystem nach dem anderen. Tests machen Refactoring sicher.
Stück für Stück ersetzen
Martin Fowler hat das den Strangler Fig Ansatz genannt, nach der Würgefeige, die langsam um ihren Wirtsbaum herumwächst. Du stellst das alte System hinter ein Gateway, baust eine Funktion neu, leitest diesen Traffic dorthin und wiederholst das, bis das alte System leer ist. Investition und Ertrag kommen, wie er schreibt, "gradually and visibly".34
Beispiel: eine Würgefeige bei einem Münchner Händler
Ein erfundener Sporthändler in München hat einen Flask Shop aus Vibe Coding, in dem Checkout, Suche und Konten verknäult sind, und er kann den Verkauf nicht anhalten.
- Erster Monat: einen Reverse Proxy vor den Shop stellen und Tests für den Checkout schreiben.
- Zweiter Monat: die Produktsuche neu bauen und
/searchdorthin leiten. Alles andere geht weiter an Flask. - Dritter bis fünfter Monat: Konten und dann den Checkout umziehen, einen nach dem anderen, jeweils hinter denselben Tests.
- Bekommt die alte App keinen Traffic mehr, wird sie abgeschaltet. Zu keinem Zeitpunkt war der Shop geschlossen.
Neu schreiben
Das ist für Systeme, die grundlegend kaputt sind: die falsche Datenbank für die Aufgabe, keine Tests, Code, den niemand warten kann. Neuschreiben hat einen langen und schlechten Ruf. Joel Spolsky nannte es, Code von Grund auf neu zu schreiben, "the single worst strategic mistake that any software company can make", mit Netscape als Beispiel.35 Schreib nur neu, wenn du verstehst, warum der erste Versuch gescheitert ist, erfahrene Leute die architektonischen Entscheidungen treffen, Spezifikationen vor jeder Zeile Code stehen und das Budget es aushält, dass das Neuschreiben länger dauert als geplant.
Die Architektur ist der Teil, den du nicht abgeben kannst
KI Assistenten fürs Programmieren können die Fragen nach der Größenordnung nicht stellen, die niemand in den Prompt geschrieben hat, und nicht die Abwägungen treffen, die entscheiden, ob ein System hält. Der teuerste Code ist der, den du zweimal schreibst. Die günstigste architektonische Entscheidung ist die, die vor der ersten Zeile fällt.
Wenn du etwas baust, das halten soll:
- Nimm dir Zeit für die Architektur vor dem Code. Skizziere das Datenmodell, benenne die Zugriffsmuster und plane für die zehnfache Größe, die du erwartest.
- Wähl langweilige Technologie. Dan McKinleys Essay mit diesem Titel sagt es gut: Jede Firma bekommt nur ein paar "innovation tokens", also gib sie für das aus, was dich unterscheidet, nicht für die Datenbank.36 Rails, Django, Phoenix und Laravel haben bekannte Wege zur Skalierung. Ein neues Framework hat unbekannte Arten zu scheitern.
- Hol früh Erfahrung dazu. Eine erfahrene Person, die die architektonischen Entscheidungen trifft, erspart ein Team, das sie später repariert.
- Teste ohne Ausnahme. Tests sind der einzige Weg, Code sicher zu ändern.
- Nutz KI für die Umsetzung, nicht für den Entwurf. Lass sie den Code schreiben, der deine Architektur umsetzt. Lass sie nicht die Architektur wählen.
Ich habe Projekte aus Vibe Coding im eCommerce, in der Finanztechnologie und im SaaS neu aufgebaut. Die Muster sind immer dieselben: das falsche Framework für die Aufgabe, fehlende Indizes in der Datenbank, keine Trennung der Zuständigkeiten und der Glaube, "es läuft" heiße "es skaliert". Wenn du dieselbe Geschichte von der anderen Seite einer Integration sehen willst, wo ungeprüfter KI Code eines Anbieters meine Kunden echtes Geld gekostet hat, lies die teure Lektion, wenn KI generierter Code auf Produktion trifft.
Der Prototyp aus Vibe Coding, der heute läuft, wird das Altsystem, das dich morgen blockiert. Bau für morgen.
Footnotes
-
Andrej Karpathy, Beitrag auf X, der "vibe coding" einführt, 2. Februar 2025, https://x.com/karpathy/status/1886192184808149383
-
Joel Becker, Nate Rush, Elizabeth Barnes und David Rein, Measuring the Impact of Early 2025 AI on Experienced Open Source Developer Productivity, METR, arXiv:2507.09089, 2025, https://arxiv.org/abs/2507.09089
-
Hammond Pearce und andere, Asleep at the Keyboard? Assessing the Security of GitHub Copilot's Code Contributions, IEEE Symposium on Security and Privacy 2022, arXiv:2108.09293, https://arxiv.org/abs/2108.09293
-
PostgreSQL Global Development Group, Multicolumn Indexes, Dokumentation von PostgreSQL, aktuelle Version, https://www.postgresql.org/docs/current/indexes-multicolumn.html
-
SQLite, Appropriate Uses For SQLite, sqlite.org, https://www.sqlite.org/whentouse.html
-
David Heinemeier Hansson, Rails 8.0: No PaaS Required, Ruby on Rails Blog, 7. November 2024, https://rubyonrails.org/2024/11/7/rails-8-no-paas-required
-
Python Software Foundation, Python support for free threading, Dokumentation von Python, https://docs.python.org/3/howto/free-threading-python.html
-
The Go Team, Go 1.18 is released, The Go Blog, März 2022, https://go.dev/blog/go1.18
-
Erlang/OTP, Frequently Asked Questions: Introduction, erlang.org, https://www.erlang.org/faq/introduction.html
-
Snowflake Inc., Caching overview, Dokumentation von Streamlit, https://docs.streamlit.io/develop/concepts/architecture/caching
-
Snowflake Inc., Understanding Streamlit's client server architecture, Dokumentation von Streamlit, https://docs.streamlit.io/develop/concepts/architecture/architecture
-
Snowflake Inc., Overview of multipage apps, Dokumentation von Streamlit, https://docs.streamlit.io/develop/concepts/multipage-apps/overview
-
Snowflake Inc., st.login, API Referenz von Streamlit, https://docs.streamlit.io/develop/api-reference/user/st.login
-
Gradio, Sharing Your App: Authentication, Leitfäden von Gradio, https://www.gradio.app/guides/sharing-your-app
-
Plotly, Background Callbacks, Dokumentation von Dash, https://dash.plotly.com/background-callbacks
-
Pallets, Using async and await, Dokumentation von Flask, https://flask.palletsprojects.com/en/stable/async-await/
-
Django Software Foundation, Django overview: sites using Django, djangoproject.com, https://www.djangoproject.com/start/overview/
-
Django Software Foundation, Asynchronous support, Dokumentation von Django 5.2, https://docs.djangoproject.com/en/5.2/topics/async/
-
Sebastián Ramírez, Concurrency and async / await, Dokumentation von FastAPI, https://fastapi.tiangolo.com/async/
-
Reflex, How Reflex Works, Dokumentation von Reflex, https://reflex.dev/docs/advanced-onboarding/how-reflex-works/
-
Philip Müller, Under Deconstruction: The State of Shopify's Monolith, Shopify Engineering, 16. September 2020, https://shopify.engineering/shopify-monolith
-
Eileen M. Uchitelle, Upgrading GitHub from Rails 3.2 to 5.2, The GitHub Blog, 2018, https://github.blog/engineering/infrastructure/upgrading-github-from-rails-3-2-to-5-2/
-
Rails Core Team, Active Record Query Interface: strict_loading, Ruby on Rails Guides, https://guides.rubyonrails.org/active_record_querying.html
-
WhatsApp, 1 million is so 2011, WhatsApp Blog, Januar 2012, https://blog.whatsapp.com/1-million-is-so-2011
-
Gary Rennie, The Road to 2 Million Websocket Connections in Phoenix, Phoenix Blog, 3. November 2015, https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections
-
The PHP Group, PHP 8.0.0 Release Announcement, php.net, November 2020, https://www.php.net/releases/8.0/en.php
-
Nikita Popov, PHP RFC: Saner string to number comparisons, PHP Wiki, umgesetzt in PHP 8.0, https://wiki.php.net/rfc/string_to_number_comparison
-
The PHP Group, Comparison Operators, PHP Handbuch, https://www.php.net/manual/en/language.operators.comparison.php
-
Composer, Releases 1.0.0 alpha1 (veröffentlicht am 3. Juli 2013) und 1.0.0 (veröffentlicht am 5. April 2016), GitHub, https://github.com/composer/composer/releases/tag/1.0.0-alpha1 und https://github.com/composer/composer/releases/tag/1.0.0
-
The PHP Group, History of PHP, PHP Handbuch, https://www.php.net/manual/en/history.php.php
-
The PHP Group, PHP 8.1.0 Release Announcement, php.net, November 2021, https://www.php.net/releases/8.1/en.php
-
W3Techs, Usage statistics and market share of WordPress, abgerufen im November 2026, https://w3techs.com/technologies/details/cm-wordpress
-
Patchstack, State of WordPress Security in 2024 (für 2023), patchstack.com, 2024, https://patchstack.com/whitepaper/state-of-wordpress-security-in-2024/
-
Martin Fowler, Strangler Fig, martinfowler.com, aktualisiert am 22. August 2024, https://martinfowler.com/bliki/StranglerFigApplication.html
-
Joel Spolsky, Things You Should Never Do, Part I, Joel on Software, 6. April 2000, https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
-
Dan McKinley, Choose Boring Technology, mcfunley.com, 2015, https://mcfunley.com/choose-boring-technology