Vortrag: Wie Monzo hunderte Deployments pro Tag sicher ausliefert
Suhail Patel erklärt, wie Monzo seine Software-Entwicklung und technische Infrastruktur aufgebaut hat, um sicher hunderte Auslieferungen am Tag zu schaffen. Dazu setzt das Unternehmen auf meinungsstarke Microservice-Abstraktionen, tenancy-basiertes Staging und statische Analysen.

InfoQ Homepage Präsentationen Move Fast, Don't Break Trust: Shipping Constantly with Humans and Beyond
Suhail Patel erläutert, wie Monzo seine Software-Engineering-Organisation und seinen technischen Stack strukturiert, um sicher hunderte Male pro Tag auszuliefern. Er teilt Einblicke in meinungsbildende Microservice-Abstraktionen, tenancy-basiertes Staging, statische Analyse-Prüfungen und Continuous Profiling und zeigt, wie eine starke interne Plattform es Ingenieuren und LLMs ermöglicht, schnell zu iterieren, ohne Zuverlässigkeit zu opfern.
Suhail Patel ist Staff Engineer bei Monzo mit Fokus auf den Aufbau der Core Platform. Seine Aufgabe umfasst den Aufbau und die Wartung von Monzos Infrastruktur, die tausende Microservices umfasst und auf Kubernetes, Cassandra und Etcd basiert. Er beschäftigt sich mit der Untersuchung abweichenden Verhaltens und stellt sicher, dass Dienste in einer sich ständig wandelnden Cloud-Umgebung zuverlässig funktionieren.
Software verändert die Welt. QCon London stärkt die Softwareentwicklung, indem es die Verbreitung von Wissen und Innovation in der Entwickler-Community fördert. QCon ist eine praxisgetriebene Konferenz für technische Teamleiter, Architekten, Engineering-Direktoren und Projektmanager, die Innovation in ihren Teams vorantreiben.
Präsentiert von: Martin Reynolds - Field CTO bei Harness, und Barani Mohandoss - Director, Cloud Governance, FinOps & E-Tools bei Trimble
Präsentiert von: David Iparraguirre - Senior Product Marketing Manager bei Datadog, und Jessica Yang - Product Manager bei Datadog
Suhail Patel: Dieser Vortrag dreht sich darum, wie wir unsere Software-Engineering-Organisation bei Monzo gestalten und wie wir technisch denken, wie wir über unseren technischen Stack nachdenken, um kontinuierlich auszuliefern, mit Menschen und darüber hinaus, mit autonomen Agenten, mit LLMs, mit allem, was als Nächstes kommen mag. Viele Organisationen, mit denen ich bis heute spreche, sehen ihre Entwicklungs- und Deployment-Umgebungen und Abläufe ungefähr so aus. Wenn Sie in der Lage sind, Ihre eigene Organisation zu gründen, vollständig AI-nativ, können Sie möglicherweise all das umgehen.
Die meisten von uns in regulierten Branchen und an Orten, die schon seit einiger Zeit existieren, ich war auf Panels und in zahlreichen Vorträgen, wo Engineering-Teams in anderen Organisationen einfach akzeptiert haben, dass dies Teil ihres Prozesses ist. Man muss eine Art Bewertung durchführen, bevor man etwas Neues beginnen möchte. Dann muss man eine ganze Reihe von Schritten durchlaufen, bevor überhaupt an Code gedacht oder dieser geschrieben wird.
Ehe man sich versieht, dauert es Tage, Wochen, Monate, um irgendetwas zu veröffentlichen. Ein großer Teil dieser Zeit war die Implementierungsphase. Systeme brauchen nun einmal Zeit zum Aufbau.
Egal, wie viel Planung man im Vorfeld betreibt, das Niederschreiben des Codes in seinem Editor kostet Zeit. Es kostet Personen. Es erfordert Tests.
Diese Gleichung hat sich inzwischen vollständig verändert. Jeder, der anders denkt, sollte meiner Meinung nach einige der Vorträge sehen, wie schnell sich Code generieren lässt. Ja, man gibt möglicherweise ein paar Cent, ein paar hundert Dollar aus, um diesen Code zu generieren.
Code lässt sich wirklich schnell generieren. Eine der zentralen Fähigkeiten, die in dieser Gleichung vielen Menschen fehlt, ist, dass man auch schnell an mehreren Varianten iterieren kann, und zwar parallel. Was früher Tage, Wochen und Monate dauerte und hohe Iterationskosten hatte, wobei man normalerweise nur Zeit für ein oder zwei Iterationen seiner jeweiligen Implementierung hatte,
kann heute in 10, 20 Iterationen der jeweiligen Implementierung münden und dabei herausfinden, welche die beste ist. Das lässt sich in Minuten und Stunden erreichen.
Es gibt im Internet sehr viel zum Thema Messung der Entwicklerproduktivität und wie man dabei wirklich objektiv vorgeht. Es gibt alle möglichen Developer-Experience-Umfragen. Man kann DORA-Frameworks verwenden.
Man kann erhebliche Mengen an Telemetrie einsetzen und wirklich datenbasiert entscheiden, wo man diese Investition platziert. Letztlich ist dies die Art von Erfahrungsbericht, die den Wert der Investition in Technologie im Hintergrund beweist, um einige der langweiligen Prozesse zu automatisieren. Wenn Ihre Ingenieure diese Werkzeuge nutzen und sich darauf verlassen und sie mit großem Nutzen einsetzen können, ist das das beste Testimonial für Ihre Organisation, denn dann merken es auch andere Führungskräfte, Leute aus dem Nicht-Tech-Bereich.
Ingenieure sehen erst, was ihnen fehlt, wenn sie die Organisation wechseln und keinen Zugriff mehr auf dieses gleiche kohärente Ökosystem von Werkzeugen haben, und umgekehrt. Bei uns kommen ständig Ingenieure an, die sagen, in meiner anderen Organisation hatten wir ein anderes System, wir konnten deutlich schneller arbeiten.
Das ist eine wirklich gute Gelegenheit zu lernen. Wenn die Developer Experience sehr viel Reibung aufweist, ist dies ein Grund, warum Ingenieure sich entscheiden könnten, das Unternehmen zu verlassen. Darüber sollten wir uns keinen Illusionen hingeben.
Das Tempo der Veränderung nimmt ebenfalls deutlich zu. Viele Unternehmen investieren stark in AI, um ihre bestehenden Ingenieure zu ergänzen. Letztlich gibt es ein Geschäftsziel.
Wir wollen die Produktivität pro Ingenieur steigern. Ingenieure sind nicht billig. Aus finanzieller Sicht besteht ein starkes Bestreben, die Produktivität pro Ingenieur zu erhöhen.
Als Ingenieure sind wir bereit, in dieser Gleichung complacent zu sein. Wir gewinnen keine zusätzliche Zeit in unserem Kalender. Wir übernehmen mehr Arbeit.
Daher entsteht ein höherer Druck, diese Änderungen auszuliefern. Ein Teil dieser Ermächtigung soll auch außerhalb des Engineering-Bereichs ausgeweitet werden. Werkzeuge wie Claude und Cursor geben Disziplinen im Umfeld des Engineering wie Designern und Produktmanagern Zugang und Erlaubnis, mit Code zu prototypisieren und Änderungen direkt in Ihren primären Codebasen vorzunehmen.
Ich habe an das erste Mal gedacht, als ich Photoshop heruntergeladen habe und dachte, ich könnte ein Designer sein. Was war das für eine Illusion. Oder das erste Mal, als ich FrontPage öffnete und dachte, ich könnte etwas für das Web gestalten, all den generierten Code sehend. Heutzutage, wenn ein Produktmanager oder Designer einen kleinen Bug hat, eine technische Implementierungsfrage oder eine Frage zu Daten hat, zum Beispiel: Was sind unsere aktuellen Geschäfts-KPIs und wie entwickeln sie sich? Warum Zeit damit verbringen, ein Ticket zu erstellen, um eine Datenperson oder eine Engineering-Person zu fragen, ob sie diese bestimmte Sache behebt oder dieses bestimmte Problem untersucht, wenn man einen Versuch über ein LLM starten kann, das es für einen löst?
Auf der anderen Seite sind selbst einige der größten Unternehmen mit zehntausenden Ingenieuren und zahlreichen Teams für Change Management, Softwaretest und Softwarequalität nicht immun gegen das Tempo der Veränderung und die Auswirkungen auf Zuverlässigkeit und Verfügbarkeit. Es gab vor Kurzem einen Artikel im Financial Times. Er ging extrem viral, auf allen Social Media und Hacker News und allen möglichen Orten, und behauptete, AI sei schuld an einer Reihe von Zuverlässigkeitsausfällen bei Amazon.
Amazon musste eine Entgegnung auf seinem Blog veröffentlichen, weil der Artikel besagte, Senior-Ingenieure müssten AI-unterstützte Änderungen abzeichnen. Amazon stellte die Fakten richtig, dass es sich hierbei nicht um AI-spezifische Änderungen handelte. Es ist sehr deutlich, wie die Branche die Rolle von AI in der Softwareentwicklung betrachtet.
Sie mussten sich rechtfertigen, sie mussten eine öffentliche Entgegnung herausgeben, warum dies nicht der Fall war. Abgesehen davon, ob die Entgegnung auf eine menschliche oder AI-getriebene Änderung zurückzuführen war, gab es ein gemeinsames Ziel: Dinge nicht kaputtzumachen.
Niemand wacht morgens auf und weist seinen Agenten an, schlechten Code zu schreiben, so wie ein Mensch nicht eines Tages aufsteht, um schlechten Code zu schreiben. Niemand möchte wegen Ausfällen in den Nachrichten sein. Auf der anderen Seite finde ich es faszinierend zu sehen, welche Prozesse Unternehmen nach einem Vorfall oder einer schiefgelaufenen Bereitstellung einführen.
So entstehen Regeln in unserer Branche, etwa dass wir nicht freitags deployen. Eine wirklich gängige Praxis ist die Einführung von Change Approval Boards, insbesondere in großen Finanzinstituten. Eine Gruppe von Personen, meistens ziemlich missmutig dreinschauend, wie auf dem Foto, typischerweise aus einer Reihe von Bereichen wie Risk und Compliance, Platform, Security und einigen weiteren Bereichen, hat die Befugnis, eine Veröffentlichung notfalls zu stoppen und zu verzögern.
Dann landet man in wirklich miserablen und unproduktiven Meetings wie diesen, um jede kleine Änderung auszuliefern.
In der Praxis werden diese Gremien notwendig, weil Umfang und Art dieser Änderungen einen großen Blast Radius haben oder Änderungen betreffen, die viel Vertrauensbildung erfordern. Sie können sich Ihr Vertrauen nicht einfach per Vibe Coding erarbeiten. Man gelangt an einen Punkt, an dem man bei bestimmter Software das Vertrauen verliert, Änderungen vorzunehmen. Dann herrscht große Trägheit bei den Ingenieuren, selbst einen einfachen Konfigurationswert zu ändern. Dieses Risikoprofil wird selbst mental exponentiell höher. Selbst technisch gesehen mag dieses Risikoexposure recht gering sein.
Mein Name ist Suhail. Ich bin einer der Principal Engineers bei einem Unternehmen namens Monzo mit Sitz in London. Ich arbeite in der Platform-Gruppe.
Ich helfe dabei, die Plattformgruppe zu leiten, in der wir die gesamte zugrunde liegende Infrastruktur, Bibliotheken und Tools von Monzo bereitstellen, damit sich Ingenieure auf den Aufbau einer Bank konzentrieren können und sich nicht um Infrastruktur kümmern müssen. Ich verbringe den Großteil meiner Wachzeit mit Gedanken an die Developer Experience. Wenn ich ganz zynisch sein soll: Was können Entwickler falsch machen?
Wie können sie Fehler in die Produktion bringen? Und wie kann ich verhindern, dass sie diese Fehler in die Produktion bringen? Ich weiß aus eigener Erfahrung, wie eine zersplitterte Erfahrung zu Frustration führt und dann zu mehr Fehlern und mehr Zwischenfällen.
Monzo ist eine auf Verbraucher ausgerichtete Retailbank in Großbritannien. Wir haben auch eine kleine Präsenz in den USA. Zudem erweitern wir in diesem Jahr unsere Aktivitäten auf europäische Märkte.
Wir haben 14 Millionen Kunden und wachsen schnell, sowohl bei unserem Privatkundenangebot als auch im Geschäftskundengeschäft.
Wir haben keine physischen Filialen. Alle unsere Banking-Funktionen werden über eine App bereitgestellt. Es gibt keine Öffnungszeiten, um die man sich kümmern müsste.
Sie erledigen all Ihre Banking-Bedürfnisse zu einer Zeit und an einem Ort, der Ihnen passt. Das bedeutet auf der anderen Seite, dass wir keine Ausfallzeiten haben. Die Menschen erwarten, dass unsere Systeme rund um die Uhr funktionieren.
Ich bekomme immer noch Benachrichtigungen von Banken, besonders von vielen mit Sitz in den USA. Falls jemand aus dem US-Fintech-Bereich hier ist, würde ich gerne mit Ihnen darüber sprechen, wie diese ihre Systeme für ein paar Stunden herunterfahren, um die Sommerzeitumstellung zu bewältigen. Kundinnen und Kunden erwarten heutzutage ein einwandfreies Online-Erlebnis, besonders wenn man sie nicht in einer physischen Bankfiliale betreuen kann.
Das bedeutet, dass wir ein vielfältiges Angebot brauchen, das zuverlässig und zugänglich ist. Ausfallzeiten und Fehler bedeuten, dass Kunden keinen Zugang zu ihrem Geld haben.
Das ist ein ziemlich altes Bild, aber es ist ein verbundener Call-Graph vieler unserer Microservices, die unsere Zahlungsinfrastruktur und auch unsere Produktangebote antreiben. Jeder Punkt repräsentiert einen Microservice, den wir auf unserer Plattform erstellen, betreiben und programmieren. Die verbindenden Linien stehen für RPC-Flüsse zwischen diesen Diensten.
Stellen Sie sich vor, wir müssten jedes Mal, wenn wir eine Änderung vornehmen, einen neuen Dienst hinzufügen oder ein neues System bauen wollen, also einen neuen Punkt oder eine neue Kante hinzufügen, einen komplexen Change-Governance-Prozess durchlaufen. Wir würden im Grunde gar nichts ausliefern. Dieser Vortrag handelt von einigen der wichtigsten Investitionen, die wir getätigt haben, um diese stetig steigende Änderungsrate zu ermöglichen.
Zum Einstieg wollen wir uns die Anatomie eines Microservices ansehen. Das ist unser Developer-Experience-Vertrag mit den Ingenieuren, die bei Monzo arbeiten. Ein Grundprinzip, das wir seit unseren Anfängen verfolgen, ist die Einheitlichkeit darin, wie wir unsere Dienste schreiben.
Für die allermeisten Microservices bauen wir nach derselben Vorlage und verwenden dieselben zugrunde liegenden Abstraktionen. Unsere Plattform- und Sicherheitsteams kümmern sich um die Infrastruktur und verankern zusätzlich die Best Practices in unserer Service-Ebene innerhalb dieser Bibliotheken. Innerhalb eines Microservices füllen die Ingenieure den oberen Teil aus und fügen ihren Code in diesen dünnen Umschlag ein, hier fügen sie ihre Geschäftslogik hinzu.
Ingenieure schreiben Kernabstraktionen wie HTTP-Handler nicht neu und müssen sich nicht um das Konsumieren aus einer Kafka-Queue, um Metriken oder das Marshalling von Daten kümmern. Sie können sich auf einen gut definierten, stark typisierten und gut getesteten Satz von Bibliotheken und Tooling verlassen. All diese gemeinsamen Bibliotheksebenen bieten von Haus aus Telemetrie im Lieferumfang.
Gerade in einer Microservices-Architektur ist eine der zentralen Fragen, den Kriminalfall aufzuklären. Wenn Kommunikationsflüsse zwischen Diensten stattfinden, wie findet man heraus, was schiefgelaufen ist und wo die Daten geflossen sind? Dazu braucht man wirklich gute Telemetrie.
Wir verankern das direkt in unserer Bibliotheksebene, damit es über alle unsere Dienste hinweg verfügbar ist, egal ob Ihr Dienst eine Anfrage oder 1 Million Anfragen pro Sekunde bedient.
Wir hatten viel Glück. Unser Plattform-Stack ist vergleichsweise modern. Wir haben unsere frühen Innovations-Chips 2016 auf Technologien wie Go, Kubernetes und Microservices gesetzt, als Teil unserer Grundlagen.
Seitdem haben wir diesen Stack sehr bewusst erweitert und neue Technologien und Integrationen eingeführt. Ein zentrales Prinzip für uns ist, dass wir diese Komponenten auf Plattformebene den Ingenieuren nicht direkt bereitstellen. Stattdessen bauen wir monzospezifische Bibliotheksabstraktionen darauf auf, die eine leicht zu bedienende Abstraktion bieten, die gut getestet und gepflegt ist und vor allem die Telemetrie eingebaut hat.
Diese Fundament-Entscheidungen haben sich enorm ausgezahlt. Wir haben über 3.000 Microservices gebaut und auf diese skaliert, die in Produktion laufen, und liefern Änderungen hunderte Male pro Tag aus. Wir haben nur ein paar hundert Ingenieure und liefern viele hundert Male am Tag aus.
Ingenieure müssen die Semantik von Kubernetes oder YAML oder komplexe Kafka-Bibliothekslogik nicht verstehen, um Dienste zu schreiben und Dinge auszuliefern.
Sie wollen zentrale Primitive und zentrale Abstraktionen haben, die zuverlässig und dauerhaft funktionieren. Wir pflegen ein Monorepo mit allen unseren Diensten, und in diesem Monorepo gibt es einen Bootstrap-Generator, der eine konsistente Ordnerstruktur für jeden Dienst ausgibt. Wie erwähnt sind die allermeisten unserer Microservices in Go geschrieben.
Das reduziert den kognitiven Aufwand für Ingenieure. Egal, ob Sie schon lange dabei sind oder neu dazugekommen sind, man lernt das Muskelgedächtnis, sich in einem Dienst zurechtzufinden. Alle Dienste sehen im Wesentlichen gleich aus.
Sie folgen derselben Ordnerstruktur, und man erwartet, dass die Datenbanklogik im dao-Ordner liegt. Man erwartet, dass die Queue-Consumer-Logik im consumer-Ordner liegt. Man erwartet, dass die RPC-Ebene im handler-Ordner liegt.
Man weiß, wo sich diese Logikteile befinden. Wir verwenden exakt denselben Software-Stack durchgängig.
Ein zentrales Ziel für uns seit den frühen Tagen ist es, diese Iterationszeit wirklich schnell zu halten. Wenn man einen Microservice generiert, füllt man als Erstes eine API-Spezifikation in einer proto-Datei aus. Wir haben Generatoren auf diesen proto-Dateien geschrieben, um den Code hinter den Kulissen in RPCs zu kompilieren, die in unsere Service-HTTP-Bibliotheken eingebettet sind und die Telemetrie im Lieferumfang enthalten.
Diese Definition kann Dinge wie Validierung umfassen, die über die Standard-Datentypen hinaus automatisch generiert wird, die man in protobuf bekommt. Das erzeugt hinter den Kulissen eine ganze Menge Golang-Code, sodass Ingenieure keine HTTP-Anfragen von Hand schreiben und kein Unmarshalling und keine Konvertierung von Daten in Go-Typen vornehmen müssen. Testen und Mocking werden deutlich einfacher, weil man mit statischen Typen arbeitet.
Wir verwenden proto-Dateien, um deutlich mehr als RPC-Code zu generieren. Zum Beispiel generieren wir auch Queue-Handling-Logik. Queue-Handling folgt demselben Grundprinzip.
Man transportiert Daten über irgendeinen Transport-Bus, also kann man das als Typ definieren. Wir generieren alle Kommunikations-Primitive hinter den Kulissen. Ingenieure können eine Event-Payload-Definition sowie alle Validierungskriterien angeben, ähnlich wie bei RPCs.
Auch hier haben wir ein proto-Plugin, das die Kafka-Consumer- und Producer-Logik generiert, die in unsere Bibliotheken einbindet. Ingenieure können mit diesen stark typisierten Objekten und automatisch generierten Funktionssignaturen arbeiten und müssen sich um den zugrunde liegenden Transport nicht kümmern. Ein großer Vorteil, den wir als Infrastrukturteam und für unsere Sicherheitsteams bekommen, ist, dass sich Bugfixes, neue Funktionen oder Performance-Verbesserungen durchgängig anwenden lassen.
Wir generieren einfach Ihren Service-Stub neu, und Sie erhalten diese Vorteile umsonst.
Das wird sich als ein wiederkehrendes Muster zeigen. Wir haben viel Zeit damit verbracht, unsere eigenen Tools von Grund auf zu bauen und zu betreiben, und auch Open-Source-Tools genutzt, und dabei mit einer tiefen Integration im Sinn, um in unsere Ökosysteme zu passen. Viele dieser Tools sind mit Blick auf Modularität gebaut.
Wir wollen die Funktionen, die diese Tools und Pakete zu bieten haben, verfügbar machen, aber auf eine recht meinungsstarke Weise und mit allem, was dazugehört, statt einfach Tools über die Wand zu werfen und zu sehen, was hängen bleibt. Unser Ziel ist es, unsere Zeit und Energie darauf zu verwenden, Ingenieuren zu helfen und es ihnen zu erleichtern, das Richtige zu tun, sie auf den gepflasterten Weg zu bringen. Es so überzeugend, einfach, schnell und sicher per Voreinstellung zu machen, dass sie sich entscheiden, Software auf der gepflasterten Straße zu bauen.
Dann machen wir es wirklich einfach, diese Software mit Zuversicht zu veröffentlichen.
So glaube ich, baut man eine wirklich gute Developer Experience: wenn man sich hyperfokussiert darauf konzentriert, Ingenieuren zu helfen, das Richtige gut zu bauen, und dann diesen Code schnell, sicher und iterativ in Produktion zu bringen und freizugeben. Wenn ein Ingenieur eintritt und, um die Berechtigung zu erhalten, an unserem Backend zu arbeiten und Code in Produktion zu deployen, durchlaufen wir einen sehr strukturierten Backend-101-Leitfaden, der einen durch alle unsere Tools und all diese Primitive führt. Einmal gelernt, bleibt das während der gesamten Karriere bei Monzo präsent.
Wir zeigen euch alle unsere Tools, wie wir Systeme schreiben, unsere Ansichten zum Schreiben von Go, unsere Logik, wie wir Code strukturieren, unsere Microservices-Architektur und vieles mehr. Einer der groessten Vorteile besteht darin, dass wir diese Dokumentation, all die Standardisierung, die wir im Kopf haben, als Skills kodifizieren und in LLM-Tools hinterlegen koennen.
Diese LLM-Tools halluzinieren keine Service-Pakete und Service-Strukturen. Sie koennen den Code auf aehnliche Weise durchsuchen wie ein Ingenieur. Wir koennen diese Dokumentation direkt zugaenglich machen, weil wir die Zeit und Energie investiert haben, alle unsere Tools und Prozesse zu standardisieren. Wir haben sie fuer Menschen dokumentiert, und jetzt koennen wir sie auch dazu nutzen, Ingenieure zu unterstuetzen.
Zurueck zu dieser Service-Anatomie: Automatisierte Tools wie Claude und Cursor lernen unsere Prozesse zum Schreiben von Code genau wie Menschen und halten sich an die Service-Struktur. Ein weiterer grosser Vorteil ist, dass wir in einem Monorepo ueber 3.000 Beispiele haben, auf die man verweisen kann. Diese KI-Tools lernen staendig unsere konsistenten Muster kennen und koennen im Hintergrund auf implementierte Beispiele zugreifen.
Sie halluzinieren nicht einfach irgendwelche Dinge aus dem Nichts. Am Ende besteht jeder Service aus ein paar Tausend Zeilen tatsaechlicher Geschaeftslogik. Diese Groesse ist fuer eine Gruppe von Ingenieuren wirklich gut verstaendlich.
Man kann einen Pull Request einreichen, und es ist etwas, das sehr leicht zu verstehen ist. Es laesst sich auch innerhalb eines Kontextfensters sehr einfach referenzieren. Ziemlich viele unterschiedliche Services sind in Echtzeit beteiligt, wenn man eine Transaktion auf unserer Plattforma durchfuehrt.
Wenn man beispielsweise seine Monzo-Karte oder eine beliebige Zahlungskarte durch den Terminal zieht, sind viele Services an diesem Ablauf beteiligt, um eine Entscheidung zu treffen.
Wir haben ein sehr kleines Zeitbudget, um darauf zu antworten, ob wir diese bestimmte Zahlung akzeptieren oder ablehnen. Dinge wie die Pruefung, ob genug Geld auf dem Konto ist und ob es sich um Betrug handelt, um sicherzustellen, dass wir uns gegen Finanzkriminalitaet schuetzen, und eine ganze Reihe weiterer Bereiche, die wir kategorisieren muessen, damit eine bestimmte Zahlung akzeptiert wird. Eine zentrale Herausforderung besteht darin, dass Ingenieure staendig Aenderungen am System vornehmen, um sich an das sich schnell veraendernde Umfeld anzupassen.
Ingenieure moechten diese Ablaeufe Ende-zu-Ende testen koennen. Wir haben einen starken Anreiz, sie dazu zu ermutigen. Im Kern sind wir eine Bank.
Ein Grundprinzip einer Bank ist die Annahme und Abwicklung von Zahlungen. Genau das moechten wir kontinuierlich Ende-zu-Ende testen. Wir wollen, dass Ingenieure Vertrauen in die Aenderungen haben, die sie ausliefern.
Historisch gesehen hatten wir keine besonders gute lokale Entwicklungserfahrung, wenn es darum ging, den kompletten Stack zum Laufen zu bringen.
Stellt euch vor, um etwas in diesem Ablauf zu aendern, muesstet ihr 250 Microservices hochfahren, jeder mit seinen eigenen Abhaengigkeiten, Konfigurationen und vielem mehr. Darueber hinaus laufen auch noch Machine-Learning-Modelle als Teil dieser Pipeline. Machine-Learning-Modelle bedeuten, dass man eine Python-Umgebung einrichtet und zu den Python-Goettern betet, dass dies das pyenv sein moege, das fuer mich funktioniert, mit dieser Version von pip und dass alle Abhaengigkeiten in Ordnung kommen.
Das ist ein kleiner Albtraum. Wenn das ueber ein breites Spektrum von Ingenieuren hinweg geschieht, ist das etwas, das wirklich komplex zu verwalten ist. Stellt euch vor, wir wollten das auch auf eine Gruppe von Nicht-Ingenieuren ausweiten.
Stellt euch vor, man bittet einen Produktmanager, Python zu installieren, zu den Python-Goettern zu beten und ein Opfer darzubringen, und hoffentlich kann er dann das Machine-Learning-Modell zum Laufen bringen. Er haette enorme Reibung, und genau das wollen wir loesen.
Es gab vor ein paar Jahren eine wirklich gute Praesentation von Soam, der Stripe vertrat, in der sie darueber sprachen, wie sie dieses Problem geloest haben. Stripe stellte entfernte Instanzen bereit, die pro Ingenieur zugewiesen wurden. Ein Ingenieur konnte eine oder mehrere Instanzen haben.
Sie bauten Tooling, um Code automatisch von ihrem lokalen Rechner in diese Dev-Box zu synchronisieren. Im Wesentlichen war dies eine Instanz, die in der Cloud lief, eine Menge Code auscheckte, und man konnte den gesamten Stripe-Stack oder einen Teil des Stripe-Stacks auf dieser Maschine hochfahren. Sie arbeiteten stark an der Benutzererfahrung, damit es sich anfuehlte, als entwickle man auf seinem lokalen Rechner.
Es fuehlte sich an, als wuerde man die Aenderung lokal ausfuehren und testen. Genau diese Erfahrung wollten wir nachbilden. Wir wollten Ingenieuren ermoeglichen, lokal schnell zu entwickeln und zu iterieren, aber die tatsaechliche Logik dann auszufuehren, wenn es um den vollstaendigen Ende-zu-Ende-Test in einer entfernten Umgebung ging.
Wir begannen mit einer sehr grundlegenden Praemisse. Wenn man eine Aenderung in einer Teilmenge von Services vornehmen moechte, sollte man nicht die gesamte Kette von Services ausfuehren muessen, um die Aenderung Ende-zu-Ende zu testen. Viele der von mir erwaehnten Services haben ihr eigenes Datenmodell, Abhaengigkeiten von Drittanbietern und viele andere Dinge.
Wir wollten diese Reibung unseren Ingenieuren nicht aufbuergern. Wir hatten einige Optionen auf dem Tisch. Wenn man es aus einer First-Principles-Perspektive betrachtet, ist eine Idee, sich stark auf Mocking und Testvertraege sowie aufgezeichnete Ausgaben und Mocking ueber Service-Grenzen hinweg zu stuetzen.
Diese Emulation bringt einen nur begrenzt weit. Wir fanden, dass der sicherste Ansatz darin besteht, den Code tatsaechlich selbst auszufuehren, statt zu versuchen, seine Nebeneffekte zu emulieren oder zu verschleiern. Wir haben eine voll funktionsfaehige Staging-Umgebung, die eine Kopie all unserer Services ausfuehrt und bei der die gesamte Integration fuer einen uebernommen wird.
Wir betreiben unseren gesamten Plattform-Stack auf dieser Staging-Umgebung. Sie ist vollstaendig konfiguriert und voll betriebsbereit. Wir haben sie zum Beispiel in einen Test-Rack integriert, der direkt mit MasterCard und vielen anderen Bereichen kommuniziert.
Natuerlich konzentrierten sich unsere Ingenieure darauf, ihre Aenderungen hauptsaechlich in der Staging-Umgebung zu testen und ihre Aenderungen Ende-zu-Ende zu pruefen. Die Ingenieure deployten im Wesentlichen aus einem Branch heraus und brachten das auf die Staging-Umgebung. Als wir mehr Ingenieure hinzunamen, stiessen wir auf zahlreiche Services unter Konkurrenz.
Wenn man zum Beispiel etwas im Account-Service aendert, haben wir nur einen einzigen laufenden Account-Service. Die Ingenieure standen in Konkurrenz, um Aenderungen an diesem Account-Service vorzunehmen. Mehrere Ingenieure wollten ihren Code ausfuehren.
Wir mussten einen Weg finden, damit zahlreiche Ingenieure denselben Service ausfuehren koennen, ohne die gesamte Plattform laufen zu lassen und ohne den gesamten gemeinsamen globalen Namespace zu stoeren.
Ein kurzer Ueberblick dazu, wie unsere Staging-Umgebung funktioniert. Wenn eine Anfrage in unserer Staging-Umgebung eingeht, routen wir die Anfrage einfach ueber Standard-HTTP an den Service, der im selben Staging-Namespace laeuft. Angenommen, ihr habt den Pink-Service auf der linken Seite.
Er moechte mit dem Aqua-Service am Boden kommunizieren. Er kann in diesem Aufrufpfad von einer Reihe anderer Services abhaengig sein. Er tauscht einige Anfragen im Ping-Pong-Verfahren aus und sendet die Antwort direkt zurueck.
Was wir getan haben, ist, eine Ebene zu bauen, in der Ingenieure ihren eigenen individuellen Namespace innerhalb der Staging-Umgebung erstellen und nur die Services deployen koennen, die sie geaendert haben. Wir nennen dieses System Tenancies. Das bedeutet, dass ein Ingenieur nicht alle Services ausfuehren muss, die Teil der Ende-zu-Ende-Anfragekette sind, die er testet.
Er muss nicht alle 250 Services kennen und in einer voellig separaten Umgebung zum Laufen bringen.
Das bedeutet, dass er seine geaenderten Bereiche Ende-zu-Ende testen kann. Wenn zum Beispiel ein Service Teil dieses Pfades ist und auf ihn zugegriffen wird, ruft er in den gemeinsamen Namespace auf. Ausserdem vermeiden wir, dass pro Ingenieur dedizierte Infrastruktur hochgefahren wird.
Ein Ingenieur kann eine oder mehrere Tenancies gleichzeitig laufen haben. Diese Tenancies sind Namespaces in unserer Staging-Umgebung und lassen sich daher sehr schnell bereitstellen. Wenn ihr beispielsweise mit Kubernetes gearbeitet habt, haben wir einen Kubernetes-Namespace.
Dies erweitern wir auch um andere Infrastruktur-Abstraktionen. Ein Kubernetes-Namespace ist sehr einfach zu erstellen und sehr einfach zu zerstoeren. Dies sind Namespaces in Kubernetes, in Cassandra und in Kafka.
Wir haben ein intelligentes Handling davon auf unserer Bibliotheksebene. Wenn eine Anfrage eingeht und ueber eine Tenancy geroutet wird, routen wir diese Services, die nicht in der Tenancy laufen, intelligent zurueck in denselben gemeinsamen Staging-Namespace.
Das funktioniert bidirektional. Ein Service im gemeinsamen Namespace kann in die Tenancy aufrufen, wenn er nach aussen routen muss. Die Methode dazu ist nicht besonders ausgefeilt. Was wir im Wesentlichen tun, ist einen Header hinzuzufuegen. Wir fuegen jeder Anfrage einen Header hinzu, aehnlich wie man einen Header fuer OpenTelemetry oder jede andere Art von Information ueber Anfragegrenzen hinweg mitgibt. Wir fuegen einen Header hinzu, der sagt, dies laeuft als Teil einer Tenancy, daher musst du vielleicht etwas Besonderes bei deinem Anfrage-Routing tun. Wir greifen in das gesamte Routing dieser Anfragen an unserem Service Mesh ein und machen dieses intelligente Routing.
Das funktioniert sehr gut für zustandslose Anfragen. Außerdem wollen wir eine gegenseitige Kontamination von Daten vermeiden. Nehmen wir zum Beispiel an, dass ein Service in diesem gemeinsamen Namespace deployt ist, aber Sie irgendwelche fehlerhaften Daten erzeugen.
Diese Daten mögen für die Services, die sie annehmen, gültig aussehen und alle Validierungsprüfungen bestehen. Zum Beispiel sind sie aus logischer Sicht betrachtet trotzdem fehlerhaft. Sie sollten die Queue und die Datenspeicher des gemeinsamen Service nicht dauerhaft kontaminieren.
Wir haben dafür eine Handhabung auf der Ebene unserer Shared Library implementiert. Jede unserer gemeinsamen Komponenten unterstützt Tenancies. Sie verstehen, was es bedeutet, eine Anfrage zu verarbeiten, die aus einer Tenancy stammt.
Zum Beispiel bedeutet das auf unserer Cassandra-Ebene, dass diese Daten in einen eigenen Container segmentiert werden. Auf unserer Kafka-Ebene bedeutet es, dass die Kafka-Nachricht in ein eigenes Topic und eine eigene Queue geroutet wird und nicht das Haupt-Topic verschmutzt.
Das bedeutet, dass man nicht mit einer Queue-Nachricht endet, die in einer Tenancy erzeugt wurde und zwar gültig, aber unlogisch sein kann, und die eine Queue blockiert, weil der Code zu ihrer Verarbeitung nicht im globalen Namespace vorhanden ist. Wie ich bereits erwähnt habe, sind diese Tenancies sehr leichtgewichtig und können in wenigen Minuten provisioniert werden, sodass wir Hunderte und Tausende von ihnen gleichzeitig laufen lassen können. Wenn eine Tenancy nicht mehr benötigt wird, können wir sie genauso schnell wieder herunterfahren.
Dieses Paradigma hat sich hervorragend bewährt, um das Problem der Konkurrenz zwischen Ingenieuren zu lösen, weil wir nun all diese Tenancies für alle unsere Ingenieure provisionieren können und einzelne Ingenieure zahlreiche Tenancies gleichzeitig nutzen können. Dieses Paradigma konnten wir auch erweitern, um es für Akzeptanztests zu nutzen. Früher liefen unsere Akzeptanztests auf unserer Staging-Umgebung.
Diese Akzeptanztests mussten nacheinander geplant werden und waren fehleranfällig, wenn ein Ingenieur während des Tests eine Änderung an einem Service deployte, der Teil des Akzeptanztests war, möglicherweise mit fehlerhaftem Code. Wir haben das Vertrauen in diese Tests erhöht, und jetzt führen wir diese Akzeptanztests als Teil eines Pull Requests durch, weil wir in der Lage sind, diese Testlogik selbst in einer Tenancy zu deployen.
Sobald alle Ihre Änderungen vorbereitet sind, können Sie einen Pull Request einreichen und um eine Überprüfung durch die Code-Owner des jeweiligen Service bitten. Im Hintergrund, während Sie eine menschliche Überprüfung einholen, führen wir bereits eine Reihe von CI-Prüfungen durch und validieren die Struktur und die Logik Ihres Codes. Bevor wir diese Prüfungen verpflichtend machen, fügen wir kontinuierlich weitere Prüfungen hinzu.
Wir entwickeln kontinuierlich neue Prüfungen auf der Grundlage der Erkenntnisse, die wir gewonnen haben. Wir stellen sicher, dass wir unsere bestehende Codebasis bereinigen, damit Ingenieure nicht durch fehlgeschlagene Prüfungen aus dem Konzept kommen, die durch Code entstehen könnten, der nichts mit ihrer Änderung zu tun hat. Das bedeutet, dass diese Prüfungen ein wirklich hochwertiges Signal für unsere Ingenieure darstellen und Ingenieure nicht dazu übergehen, diese Prüfungen zu ignorieren.
Was wir erreicht haben, ist, dass wir eine erhebliche Hürde für die Umgehung dieser Prüfungen eingebaut haben.
Zum Beispiel müssen Sie an mehrere Personen eskalieren und praktisch ein Notfallverfahren durchlaufen und begründen, warum Sie diese Prüfungen umgehen. Wir wollen, dass der gepflasterte Weg das korrekte Verhalten ist, der Weg des geringsten Widerstands, indem wir all diese Prüfungen vorschreiben und ein wirklich hohes Signal sicherstellen. Ich bin fest davon überzeugt, dass es der größte Bereich technischer Hebelwirkung ist, seine Prüfungen robust, schnell und leicht verständlich zu machen.
Das ist nichts, was man einfach mit einem fertigen Produkt kaufen könnte. Es gibt einige Anbieter, die darauf hinweisen, dass man sein CI so viel besser machen kann. Ich denke, im Grunde muss man dies als technische Disziplin in die eigene Organisation einbetten.
Es erfordert viel Aufwand, CI-Prüfungen zu einem hohen Signal zu machen, und ich halte das nicht für optional. Wenn ein Vorfall durch einen Codefehler auftritt, sollte man sich konkret die Frage stellen: Wie kann ich das in eine Prüfung umwandeln, damit dieser Fehler in Zukunft nicht mehr auftritt?
Wenn Ihre CI-Prüfungen langsam oder fehleranfällig sind, ist das ein wirklich hervorragender Bereich, um gemeinsam mit LLM-Tools die Engpässe zu finden und zu beseitigen. Jeder PR, der in Produktion geht, erfordert eine menschliche Überprüfung, sodass alle Services einen menschlichen Code-Owner haben, der als verpflichtender Prüfer sicherstellt, dass eine Änderung den Ownern der Services gut kommuniziert wird. Unsere Services ändern sich ständig.
Wir haben Ingenieure, die überall im gesamten System beitragen. Es ist nicht so, dass Ingenieure nur zu Services innerhalb ihres Teams beitragen. Ingenieure tragen zum gesamten Microservice-Ökosystem innerhalb von Monzo bei, weil alles in einem Monorepo liegt und alle Service-Strukturen gleich aussehen, sodass man dieses Element der Vertrautheit hat.
CI gibt Ihnen die Sicherheit, dass Sie die Sache korrekt gebaut haben. Wir können die Logik des Codes prüfen und sicherstellen, dass Sie Ihr Error-Handling und Ähnliches korrekt umsetzen, was Zeit und Energie für unsere menschlichen Reviewer und Teams freisetzt, damit diese prüfen können, ob wir das Richtige gebaut haben.
Das sind die beiden grundlegenden Dinge. Sie wollen sicherstellen, dass Sie das Richtige gebaut haben und die Produktanforderung lösen, die Sie lösen wollten, und dann, dass Sie die Sache korrekt gebaut haben. Stellen Sie sicher, dass Ihre gesamte Logik stimmt. Wir investieren einen erheblichen Teil unserer Zeit und Energie in deterministische Tests, ob wir etwas korrekt gebaut haben, damit wir unsere menschlichen Kapazitäten darauf verwenden können sicherzustellen, dass wir das Richtige gebaut haben.
Ein Bereich, der kontinuierlich weiterentwickelt wird und viel Aufmerksamkeit erhält, ist die Fähigkeit, Code zu analysieren und zu reviewen. Das kann durchaus ein Vorteil sein, aber falsch implementiert erhält man einfach eine Menge Nits, also Dinge, die völlig irrelevant sind, keine großen logischen Mängel finden, sondern nur auf Tippfehler hinweisen. Aus meiner Sicht braucht man keinen ausgefallenen KI-Code-Reviewer, um all Ihre Probleme zu lösen.
Stattdessen sollten Sie die Zeit und Energie darauf verwenden, wirklich hochwertige CI-Prüfungen zu schreiben, die sich selbst bezahlt machen. Sie sind deterministisch. Wenn Sie KI-basierte Reviews einsetzen, stellen Sie sicher, dass sie ein hohes Signal liefern und tatsächlich auf Probleme in der Geschäftslogik prüfen, also darauf, dass Sie für das richtige Produkt gebaut haben, statt auf triviale semantische Nits.
Bei uns introspektieren diese CI-Prüfungen, die wir gebaut haben, den Code eingehend, um fehlerhafte Logik oder Verhaltensweisen zu kennzeichnen, die wir missbilligen.
Wir setzen sehr stark auf den Go AST Parser und neue Pakete wie analysis, um diese Prüfungen zu erstellen. Wir haben eine Reihe kleiner, unabhängiger Prüfungen, die sehr schnell laufen. Man kann sie lokal auf seinem Rechner ausführen, um nach Mustern zu suchen, die wir stark missbilligen.
Man kann sie als Teil eines Git-Push als Pre-Push-Hook ausführen. Zum Beispiel haben wir eine Prüfung, die feststellt, ob Sie Cross-Service-Pakete importieren, was wir missbilligen. Beispielsweise wollen wir sicherstellen, dass Sie nur dann mit Code aus einem anderen Paket kommunizieren und ihn importieren, wenn die Kommunikation über Servicegrenzen hinweg erfolgt, statt einfach direkt seinen Code zu importieren.
Wir wollen sicherstellen, dass Services entkoppelt sind und nicht von internen Implementierungsdetails abhängen. Dies kann auch für den Aufbau sicherer Services gelten. Für jeden unserer Services wollen wir Egress- und Ingress-Firewall-Fähigkeiten sicherstellen.
Zum Beispiel haben wir einen API-Service. Es ist ein relativ einfacher API-Service, der alle unsere Bankfilialen ausgibt. Ich glaube, es ist Teil einer britischen Verordnung, dass man eine API haben muss, die auf programmatische Weise alle physischen Bankfilialen liefert. Bei uns ist es eine sehr stabile API, die die leere Liste zurückgibt. Dieser Service muss keinerlei Kommunikation mit irgendeinem anderen Service aufnehmen, um eine leere Liste auszuliefern. Er sollte nicht nötig haben, mit dem Ledger oder irgendeinem anderen Drittanbieter-Service zu kommunizieren, um diese bestimmte API-Anfrage zu bearbeiten.
Denken Sie erneut an unseren Microservice-Graphen. Wir haben viele laufende Services und wollen nicht, dass Ingenieure all diese Verbindungen von Hand verwalten. Dabei macht man sehr leicht Fehler und öffnet eine Schwachstelle.
Stellen Sie sich in dieser Branches-API vor, dass eine Drittanbieter-Abhängigkeit als Teil des Software-Stacks eingeführt wurde, die mit einem Command-and-Control-Zentrum kommuniziert. Dank all dieser statischen Analyse können wir diese Firewall-Fähigkeiten auf eine wirklich schöne Weise aufbauen, die für Ingenieure leicht nachvollziehbar ist: Man fügt explizit eine Regel für die Kommunikation mit einem Drittanbieter hinzu und blockiert sofort alles andere. Wir erzeugen eine Reihe solcher Regeldateien, die neben Ihrem Code liegen und alle gültigen Egress-Möglichkeiten auflisten, die Sie von Ihrem Service aus nutzen dürfen, und checken diese zusammen mit Ihrem Service-Code und Ihren Pull Requests ein.
Diese_checks sind peer-reviewed und liegen neben dem Service-Code. Das bedeutet, dass wir eine Prüfung in die CI einbauen können, um sicherzustellen, dass diese Daten bei jeder Änderung konsistent bleiben, unsere Sicherheitslage straff bleibt und alle zufrieden sind. Beim Schreiben solcher Prüfungen war ich meist ein großer Verfechter von Tools wie Semgrep, weil sich damit statische Analyseprüfungen wirklich einfach schreiben lassen. Semgrep funktioniert außerdem über eine Vielzahl von Programmiersprachen hinweg. Man kann Prüfungen für SQL schreiben, Prüfungen für Python und für viele weitere Programmiersprachen. Man muss sich dabei nicht in die Details oder abstrakte Syntaxbäume all dieser verschiedenen Sprachen einarbeiten.
Was ich vor relativ kurzer Zeit entdeckt habe, ist, dass die Arbeit an solchen Prüfungen mit Claude ein echter Türöffner dafür war, nativere Syntaxprüfwerkzeuge zu schreiben. Es war ein absoluter Game Changer für das Testen komplexer Logik. Man möchte zum Beispiel beispielsweise die Lebensdauer einer Variable über Funktionen hinweg verfolgen, um sicherzustellen, dass sie nicht gleichzeitig verändert wird.
Ich empfehle Ihnen wärmstens, das einmal auszuprobieren. Gerade wenn man einen Korpus von Testfällen und Beispielen generiert, kann man sich einen Analyzer von Claude erstellen lassen, ohne eine Menge AST-Parsing-Code von Hand zu schreiben. Es kann eine wirklich starke Inspirationsquelle sein.
Statische Analyse ist auch ein sehr guter Weg, eine Codebasis auf einen Pfad der Konsistenz und der Konventionen zu bringen. Die Einführung einer neuen statischen Analyseregel verhindert, dass weiterer schlechter Code zusammengeführt wird, und unterstützt Bereiche, die man ablösen möchte. Anschließend kann man sich darauf konzentrieren, bestehende Verwendungsfälle zu entfernen.
Auch das ist eine wirklich wirkungsvolle Möglichkeit, LLM-Tools einzusetzen. Wenn sie in der Lage sind, diese Prüfungen über die gesamte Codebasis laufen zu lassen, können sie alle Verwendungsfälle identifizieren und dann effektiv einen Teil der Fleißarbeit übernehmen, Pull Requests stellen und einige dieser Probleme im Hintergrund beheben, und sicherstellen, dass diese Probleme behoben werden, weil man Zeit in die Erstellung einer deterministischen Prüfung investiert hat. Anstatt heutzutage eine Menge Zeit damit zu verbringen, Ausnahmen fest zu codieren, besteht eine der wichtigsten Aufgaben, wenn man einen grünen Build sicherstellen will, darin, viel Zeit damit zu verbringen, alle Ausnahmen zu bewerten und zu sagen: wird nicht behoben, oder ignorieren wir das für diese bestimmte Prüfung.
Stattdessen kann man es einfach in ein LLM einspeisen und fragen: Kannst du das beheben? Wenn nicht, füge ein Ignore-Tag hinzu.
Sobald ein Service im Einsatz ist, benötigt man Einblick darin, wie er funktioniert. Gerade in der Ära der LLMs und autonomer Systeme, die Code ausgeben, ist es sehr leicht, fehlerhaften oder schlecht funktionierenden Code zu deployen. Wir nutzen viele Open-Source-Tools wie Prometheus, Grafana und OpenTelemetry, um unsere Observability-Infrastruktur zu betreiben.
Wir investieren sehr stark in das Erfassen und Sammeln all dieser Telemetriedaten aus unseren Services und unserer Infrastruktur. Das ist nicht billig. Wir zahlen eine Menge Geld dafür.
Wir sind jedes Mal dankbar, wenn wir einen Vorfall haben und die beitragenden Faktoren identifizieren können, weil wir all diese Telemetriedaten haben. Für jeden Service, der mit unseren Batterien-inklusive-Bibliotheken und Tools in Betrieb geht, erhalten wir Tausende und Zehntausende von Datenpunkten, die wir auf ein generisches Dashboard geplottet haben. Auch hier können wir LLM-Tools Zugriff darauf geben, um bei Vorfällen bei der Fehlersuche in unseren Systemen zu helfen.
Das fließt auch in Alerts ein. Wir haben automatisierte Alerts für alle unsere Services, die auf diesen gängigen Metriken basieren und sich auf einen deployten Change zurückverfolgen lassen. Alerts werden automatisch an das richtige Team weitergeleitet, dem der jeweilige Service gehört.
Bei den meisten Backend-Systemen ist Performance-Profiling aus verschiedenen Gründen meist abgeschottet. Besondere Privilegien, man braucht Root-Zugriff auf die Maschine und Ähnliches. Nur wenige Organisationen sind bereit, Ingenieuren die Möglichkeit zu geben, Profiler in der Produktion auszuführen.
Diese Haltung muss sich wirklich ändern. Man braucht Fähigkeiten, um in seine Systeme hineinschauen zu können. Viele Sprachen bieten mittlerweile integrierte Profiler, die wirklich leistungsfähig sind und praktisch keinen oder nur einen sehr geringen Overhead verursachen.
Wir machen es Ingenieuren sehr leicht, den Go-Profiler in unseren Backend-Systemen aufzurufen, und bieten eine schöne Web-UI an, über die man diese Profile anfordern kann. Java hat wirklich gute Tools mit Flight Recorder. Wenn man Python verwendet, ist auch py-spy wirklich gut.
Sehr geringer Overhead.
Ich habe darüber gesprochen, wie man Entwicklerwerkzeuge wirklich schnell macht. In letzter Zeit habe ich viel im Bereich Data Engineering gearbeitet. Die meisten dieser Tools sind in Python geschrieben.
Es ist sehr leicht, ineffizienten Python-Code zu schreiben. Viele Objekte kopieren, etwas zwischen einer Map und einer Liste umwandeln. Man bekommt die Illusion vermittelt, dass ein einzelner Funktionsaufruf relativ billig ist.
Dann hat man eine Million Operationen, die in einem DataFrame durchgeführt werden. Diese sehr billige Logik ist sehr zeitaufwendig geworden. Eine einfache native Implementierung hätte deutlich weniger Overhead.
Es gibt reichlich Gelegenheiten, Verbesserungen und Optimierungen zu finden. Ein paar Sekunden, ein paar Dutzend Sekunden oder ein paar Minuten aus einem Entwicklerwerkzeug herauszuholen, ist eine spürbare Verbesserung. Es gibt auch Tools wie Pyroscope, mit denen man kontinuierliches Profiling betreiben kann.
Die Software wird laufend profiliert, sodass man erkennen kann, ob eine Änderung eine Verschlechterung der Performance verursacht hat.
In eine Sprache übersetzt, die Wirtschaftlichkeitsverantwortliche verstehen: Je schneller der Code läuft, desto weniger Server braucht man, desto niedriger ist die Cloud-Compute- oder Infrastrukturrechnung. Um Ingenieure dafür zu incentivieren, haben wir einen Slack-Kanal namens Graph Trending Downwards. Dort können Ingenieure mit ihren Optimierungen angeben.
Sie können der Welt davon berichten. Alle unsere Engineering-Leader, unser CTO und unsere Engineering-Direktoren schauen regelmäßig in dem Kanal vorbei und würdigen diese Optimierungen, etwas, mit dem man zur Review-Saison angeben kann. Das ist ein klarer Nutzen für die gesamte Engineering-Disziplin.
Es ist eine Tätigkeit, die in persönlichen Reviews und Feedback belohnt wird. Auch das ist eine wirklich hervorragende Möglichkeit, autonomes Codieren zu nutzen. Anstatt ihm ein großes Programm zu geben und blind zu sagen, finde alle Möglichkeiten zur Optimierung, kann man ihm ein CPU- oder Memory-Heap-Profil sowie Zugriff auf den Code geben.
Es kann Optimierungen gezielt dort ansetzen, wo es Hotspots im Code gefunden hat. Es kann zum Beispiel die Anzahl der Aufrufe einer teuren Funktion reduzieren oder gleich von vornherein die teure Funktion optimieren. Ich kenne Teams, die LLMs für das Bug-Tracking eingesetzt haben. Man hat LLMs beispielsweise in Sentry oder andere genutzte Tools eingebunden. So gibt es etwa einen Button, der sagt: Du hattest eine Exception, geh und versuche, sie zu beheben, indem du Zugriff auf den Code erhältst. Das Gleiche kann man mit Performance-Profiling machen, dann werden Ihre Ingenieure viel zufriedener sein und auch Ihre Finanzverantwortlichen.
Zum Schluss sprechen wir über das Debuggen von Systemen in der Produktion. Es ist wichtig, dass Ingenieure die Geschäftslogik debuggen können. Alles andere können Sie debuggen.
Warum sollten Sie Ihre Geschäftslogik nicht debuggen können? Manchmal bedeutet das, Service-Endpunkte abzufragen und zu sehen, in welchem Zustand sich die Daten befinden, selbst in der Produktion. Das kann man unter Berücksichtigung von Privatsphäre und Datenbereinigung anbieten.
Ich sehe in vielen Organisationen zwei Extreme. Entweder müssen Ingenieure betteln und flehen, um Zugriff auf die Produktionsdatenbank zu bekommen oder Endpunkte direkt in der Produktion aufrufen zu dürfen, unter einer gewissen Illusion von Sicherheit. Oder auf der anderen Seite müssen sie viele Logzeilen hinzufügen und viel Wackelarbeit mit Telemetrie und Deployments betreiben, um herauszufinden: Das war es definitiv nicht.
Definitiv das hier. Ich habe definitiv diese Funktion dort drüben durchlaufen, um ihre Systeme zu debuggen.
Was wir getan haben, ist, eine CLI zu bauen, die es erlaubt, direkt mit der Produktion zu kommunizieren. Diese autorisiert sich sicher auf unserer Plattform und ermöglicht es Ingenieuren, direkt mit unseren Backend-Services zu interagieren, Service-Konfigurationen zu verwalten, Operationen zu planen, mit unserer Infrastruktur zu interagieren und vieles mehr. Es ist unser meinungsbildnerischer Weg, Funktionsbausteine bereitzustellen, die eine Kette von Requests darstellen können.
Zum Beispiel das Konfigurationsmanagement. All das wird über die CLI abgewickelt. Wir haben erst kürzlich eine sehr starke Validierung in unser Konfigurationsmanagement eingebaut, um sicherzustellen, dass Ingenieure keine unlogischen Änderungen in unser Konfigurationssystem einbringen.
Wir haben auch Dinge wie Peer-Reviews eingebettet. Zum Beispiel haben wir unser eigenes Multi-Party-Authorization-System gebaut, das es erlaubt, ein zweites Paar Augen für die Prüfung von Änderungen hinzuzuziehen. Oder wenn man beispielsweise einen sensiblen RPC aufruft, braucht man die Genehmigung des codeverantwortlichen Teams, um sicherzustellen, dass man keine schlechten Änderungen in der Produktion vornimmt.
Ich sehe viele Power-User, die stolz auf ihre Kommandozeilen-History sind, auf anpassungsfähige Shell-Befehle. Vielleicht haben Sie ein wirklich ausgefallenes Postman-Skript oder diesen fantastischen curl-Befehl. Wenn man das jedoch als Tool baut, kann jeder beitragen und jeder profitiert. Einer der größten Netto-Vorteile, die wir sehen, ist, dass wir auch Nicht-Engineering-Kollegen haben, die die Pound CLI täglich nutzen. Das sind Leute, denen ein großes IPython-Notebook oder ein Postman-Skript allein nicht angenehm wäre.
Viele der Tools, die ich gezeigt habe, richten sich an Backend- und Frontend-Entwickler. Man kann auch in Bereichen wie Machine Learning und Mobile Testing Dinge automatisieren. Wir haben zum Beispiel eine ganze Menge Tooling, das ML-Service-Code generiert und ihn für Training und Testing auf Basis von Vertex AI vorbereitet, dann ein Modell nimmt, auf unserer Plattform baut und deployt.
Zum Beispiel das, was ich auf dem Bildschirm zeige: Wir haben ein CLI-Tool, das mit dem Android-Emulator interagiert, um Aufgaben zu automatisieren, die ein Android-Entwickler täglich erledigt. Gerade wenn man Android-Entwickler ist und irgendeine Form von Release-Testing durchführt oder für das QA des Releases dieser Woche verantwortlich ist, sind diese Dinge ein Geschenk des Himmels, um automatisierte Screenshots zu erstellen, Vergleiche anzustellen, automatisierte Tests auszuführen und den Emulator zu starten und Ähnliches.
Wir haben das branchenweit gelöst, und es ist sehr schön zu sehen, dass CLIs ein Comeback feiern. Hoffentlich führt das zu mehr Investitionen in CLIs. Diese Analogie lässt sich auch auf die Ermöglichung von Veränderungen durch eine breitere Gruppe von Menschen jenseits der Entwickler übertragen.
Wir wollen, dass Entwickler und Nicht-Entwickler unseren gepflasterten Weg nutzen. Unsere Tools müssen schnell und zuverlässig und gut verstanden sein. Selbst für LLMs führt der Aufruf deterministischer Tools zu einem deutlich zuverlässigeren Ergebnis.
Denken Sie an all die Provisioning-Schritte, die ein Entwickler möglicherweise durchführen muss, und an das Kompilieren bei der Arbeit an einer Änderung. Wenn Entwickler eine ganze Reihe von Verrenken aufführen müssen, um ihre Entwicklungsumgebung zu entblocken und zu den Python-, Ruby- oder Node-Göttern beten, viele zufällige Pakete installieren und Minuten oder Stunden darauf warten müssen, dass ihre Software kompiliert, geben die Leute auf und versuchen, Ihren gepflasterten Weg zu umgehen. Wenn es viel einfacher ist, sich bei einer Drittanbieter-Plattform anzumelden und zu Vercel oder einem anderen System zu shippen, dann werden sie genau das stattdessen tun.
Sie werden einen Weg finden. Damit haben Sie im Effekt immer mehr Vibe Coding ermöglicht.
Um das alles zusammenzufassen und zu bündeln: Ich habe exemplarisch einige der Tools gezeigt, die wir gebaut haben, und das sind Investitionen über das vergangene Jahrzehnt, um unsere organisatorischen Prozesse in Code zu fassen. Lassen Sie sich von dem, was wir gebaut haben, inspirieren, finden Sie die Stellen, an denen Ihre organisatorischen Prozesse bröckeln, und schaffen Sie Wirkung, indem Sie sie automatisieren und einfacher machen. Viele Unternehmen investieren erheblich in eine breitere Zugänglichkeit von LLMs, und effektiv braucht man dafür das Fundament, um das ermöglichen zu können.
Die Hürden für Investitionen in diese Tools sind deutlich gesunken und werden weiter geringer. Investitionen in diese zentralen Engineering-Fähigkeiten, wie deterministische Code-Generatoren, standardisiertes Paketmanagement, konsistentes Test-Tooling, zuverlässige Telemetrie, wirklich zuverlässige Staging-Umgebungen und Profiling, haben einen enormen Ertrag in einer Welt, in der wir Menschen und LLMs und darüber hinaus ermöglichen, und in der die Kosten der Code-Generierung deutlich billiger geworden sind.
Wir wollen diese schnelle Iteration ermöglichen, und LLMs können Entwickler verstärken, wenn all diese anderen Dinge so geregelt sind, dass die Hypothese schneller und sicherer validiert wird, statt blind zu fliegen. Indem wir auf eine kleine Auswahl von Technologie-Entscheidungen standardisieren und diese Abstraktionen kontinuierlich verbessern, ermöglichen wir Entwicklern und LLMs, sich auf das jeweilige Geschäftsproblem zu konzentrieren, statt jedes einzelne Mal die zugrunde liegende Infrastruktur neu zu erschaffen. Natürlich bauen wir nicht alles von Grund auf neu.
Wir stützen uns auf das Fundament des gesamten Engineering-Ökosystems um uns herum, mit Open Source. Wir verlassen uns auf viel Open Source und auch auf viele Drittanbieter-Dienste, aber wir projizieren die Open-Source-Technologie nicht direkt in unsere Microservices und Systeme. Wir haben einen sehr meinungsstarken Ansatz.
Wir sind dabei sehr bewusst vorgegangen. Seien Sie sich bewusst über die Erfahrung, die Entwickler machen sollen, und projizieren Sie dann die Schnittstelle, die dieser Erfahrung entspricht.
Teilnehmer 1: Frage eins: Tötet ein so meinungsstarker Stack mit all den gebauten Bibliotheken nicht das Engagement der Entwickler, weil diese nur noch die Geschäftslogik lösen, was nicht immer der spannendste Teil für einen Entwickler ist? Und damit zusammenhängend: Wenn es um Low-Level-Probleme mit hoher Performance und hohem Durchsatz geht, die gelöst werden müssen, wie können sie diese Dinge debuggen, untersuchen und tunen, wenn sie die Bibliotheken darunter nicht verstehen?
Suhail Patel: Sprechen wir über die Motivation der meisten Entwickler. Ich denke, das ist eine Illusion, die wir uns branchenweit aufgebaut haben. Entwickler wollen komplexe ingenieurstechnische Probleme lösen, und es gibt jede Menge komplexer Probleme in den Domänen, auf die sie sich fokussieren. Wenn man zum Beispiel im Bereich Zahlungen oder Finanzkriminalität tätig ist, gibt es genug interessante Arbeit darin, ML-Modelle zu tunen und immer ausgefeiltere Regeln zu entwickeln oder sicherzustellen, dass wir Zahlungen zuverlässiger verarbeiten, um diese Entwickler zu engagieren und ihre Köpfe herauszufordern. Das muss nicht unbedingt auf der Infrastrukturebene geschehen.
Die zweite Frage betrifft hohen Durchsatz und tiefes Verständnis. Wir verbringen eine erhebliche Menge Zeit damit, die Einschränkungen unserer Systeme zu dokumentieren und zu erklären. Unsere Systeme sind nicht perfekt.
Zum Beispiel kommuniziert unser RPC-Stack über HTTP. HTTP hat natürlich seine Grenzen. Möglicherweise verlässt man sich auf eine bestimmte Implementierung von HTTP.
Sie könnten fragen, warum nicht gRPC, oder irgend ein anderes Protokoll? Wir haben uns auf einen sehr starken Satz gut getesteter und meinungsstarker Frameworks verlassen, bei denen wir nachgewiesen haben, dass wir sie deutlich auf eine große Anzahl skalieren und deployen können. Wir haben nur sehr wenige Anwendungsfälle gefunden, in denen Entwickler diese Durchsatzgrenzen durchbrechen müssten.
Im Grunde lautet die Frage, die wir Entwickler stellen: Warum glauben Sie, dass Sie den gepflasterten Weg verlassen müssen? Denn wir konnten es bei dieser Größenordnung nachweisen, was meist weit über der Größenordnung liegt, die sie erreichen wollen. So wird aus einer hypothetischen Gedankenübung eine tatsächliche Realitätsübung, denn bei uns am Boden laufen Systeme, die Millionen von Anfragen pro Sekunde verarbeiten, mit den Einschränkungen, die wir gebaut haben.
Warum können Sie Ihr System nicht innerhalb dieses Rahmens bauen?
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
InfoQ Homepage Presentations Move Fast, Don’t Break Trust: Shipping Constantly with Humans and Beyond
Suhail Patel discusses how Monzo structures its software engineering organization and technical stack to ship safely hundreds of times a day. He shares insights into opinionated microservice abstractions, tenancy-based staging, static analysis checks, continuous profiling, demonstrating how a strong internal platform enables engineers and LLMs to iterate rapidly without sacrificing reliability.
Suhail Patel is a Staff Engineer at Monzo focused on building the Core Platform. His role involves building and maintaining Monzo's infrastructure, which spans thousands of microservices and leverages Kubernetes, Cassandra, and Etcd. He focuses on investigating deviant behavior and ensuring services work reliably within a constantly shifting cloud environment.
Software is changing the world. QCon London empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.
Presented by: Martin Reynolds - Field CTO at Harness, and Barani Mohandoss - Director, Cloud Governance, FinOps & E-Tools at Trimble
Presented by: David Iparraguirre - Senior Product Marketing Manager at Datadog, and Jessica Yang - Product Manager at Datadog
Suhail Patel: This is going to be a talk about how we shape our software engineering organization within Monzo and how we think technically, how we think about our technical stack to allow us to ship continuously with humans and beyond, with autonomous agents, with LLMs, whatever may come next. Many organizations that I speak to, to this day, their development and deployment environments and flows look something like this. If you're able to start your own organization, be completely AI native, you may be able to bypass all of this.
Most of us in regulated industries and places that have been around for a fair amount of time, I've been on panels and numerous talks where engineering teams in other organizations have just accepted that this is part of their process. You have to form some form of assessment before you want to start anything new. Then you have to do a whole bunch of steps before any code gets even thought about or written.
Before you know it, it takes days, weeks, months to get anything released. A large chunk of that time was the implementation phase. Systems do take time to build.
No matter how much planning you do up front, getting the code down in your editor takes time. It takes people. It takes testing.
Now, that equation has completely changed. Anyone who thinks otherwise, I think might need to see some of the talks on how quickly it can be to generate code. Yes, you might spend a couple cents, a couple hundred dollars generating that code.
Code can be generated really quickly. One of the key capabilities that a lot of people are missing in this equation is that you can iterate on multiple variations really quickly, too, in parallel. What used to take days, weeks, and months and have a high cost of iteration, usually you'd only have time to do one or two iterations of your particular implementation.
Now you could have 10, 20 iterations of your particular implementation and find which one is best. That can be achieved in minutes and hours.
There's a ton on the internet about measuring developer productivity and being really objective about it. There's all sorts of developer experience surveys. You can have DORA frameworks.
You can put in significant amounts of telemetry and be really numbers-driven on where you put that investment. Ultimately, this is the kind of testimonial that proves the value in investing in technology behind the scenes to automate some of the boring processes. When your engineers use and rely on these tools and are able to leverage them for great benefit, that is the best testimonial for your organization, because then other leaders, folks who are in non-tech, notice.
Engineers only see what they are missing when they move organization and no longer have access to that same cohesive ecosystem of tools, and vice versa. We have engineers join all the time that are like, we have this other system in my other organization, we're able to move significantly faster.
That's a really good opportunity to learn. When your developer experience has a lot of friction, this is a reason that engineers might choose to leave the company. We shouldn't be under any illusion.
The rate of change is also significantly increasing. Many companies are investing heavily into AI to augment their existing engineers. Ultimately, there is a business goal.
We want to increase the rate of productivity per engineer. Engineers don't come cheap. There is a strong desire from a financial perspective to increase the rate of productivity per engineer.
As engineers, we are willing to be complacent in this equation. We are not freeing up more time in our calendar. We are taking on more work.
Therefore, there's higher pressure to go and ship these changes. Part of this enablement also wants to be extended outside of engineering. Tools like Claude and Cursor give access and give license to engineering-adjacent disciplines like designers and product managers to go and prototype with code and make changes directly in your primary codebases.
I was thinking about the first time I downloaded Photoshop and thought that I could be a designer. What an illusion that was. Or the first time I opened FrontPage and thought I could design something for the web, seeing all of that code being generated. Nowadays, if a product manager or a designer has got a small bug or a technical implementation question or question about data, for example, what are our current business KPIs and where are they trending? Why spend time creating a ticket to go ask a data person or to go ask an engineering person to go and fix that particular thing or investigate that particular issue when you can take a shot through an LLM to go and solve it for you?
On the flip side, even some of the largest companies with tens of thousands of engineers and numerous teams dedicated to change management and software testing and software quality aren't immune to the rate of change and the impact on reliability and availability. There was a Financial Times article recently. It went extremely viral across like all the social media and Hacker News and all the various places that AI was to blame for a bunch of reliability outages within Amazon.
Amazon had to publish a retort on their blog, because the article said that senior engineers had to sign off on AI-assisted changes. Amazon did correct the record that these weren't AI-specific changes. It's very clear how the industry is viewing the role of AI in software development.
They had to justify, they had to put out a public retort on why this was not the case. Putting aside whether the retort here was due to change that was human or AI-driven, there was a shared goal in not wanting to break things.
Nobody wakes up in the morning and instructs their agent to go and write bad code, just like a human doesn't wake up one day to write bad code. Nobody wants to be in the news because of outages. On the flip side, I find it fascinating to see what processes companies put into place after an incident occurs or a deployment goes wrong.
This is how rules in our industry, like we don't deploy on Fridays, come about. A really common practice is the introduction of change approval boards, especially in large financial institutions. A group of individuals, usually looking quite miserable, like they are in the photo, typically across a bunch of areas, like risk and compliance, and platform, and security, and a bunch of other areas, go out and have the ability to go and stop and delay a release if necessary.
Then you end up in really miserable and unproductive meetings like this to go and ship every small change.
In practice, these forums become necessary because the scope and nature of these changes have a large blast radius, or changing things that need a lot of confidence building. You can't just vibe code your confidence out there. You get to a point where certain software, you lose the confidence to make change. Then you have a lot of inertia from your engineers to go and change even a simple configuration value. That risk profile even mentally becomes exponentially higher. Even technically, that risk exposure might be quite low.
My name is Suhail. I am one of the principal engineers at a company called Monzo, based in London. I work in the platform group.
I help lead the platform group, where we provide all of Monzo's underlying infrastructure, libraries, and tooling, so that engineers can focus on building a bank and not have to worry about infrastructure. I spend most of my waking hours thinking about developer experience. If I'm being quite cynical, what can developers do wrong?
How can they ship bugs to production? How can I avoid them shipping those bugs to production? I know firsthand how a fractured experience leads to frustration and then leads to more bugs and more incidents.
Monzo is a consumer-facing retail bank in the UK. We've also got a small presence in the U.S. We are also expanding into Europe geographies this year.
We have 14 million customers and growing rapidly across our personal account offering and business banking as well.
We don't have any physical branches. We power all of our banking features through an app. There's no opening hours to worry about.
You handle all of your banking needs at a time and place that is convenient for you. What that means on the other side is that we don't have downtime. People expect our systems to work 24/7.
I still get notifications from banks, especially a lot of those based in the U.S. If any folks are in U.S. fintech, I'd love to speak to you about how they're going to be taking down their systems for a couple hours to handle daylight savings. Customers nowadays expect a flawless online experience, especially if you can't service them in a physical bank branch.
That means we need to have a diverse offering that is reliable and accessible. Having downtime and having bugs means that customers don't have access to their money.
This is quite an old picture, but it's a connected call graph of many of our microservices that power our payments infrastructure and also our product offerings. Every dot represents a microservice that we build and operate and write on our platform. The lines connecting them represent RPC flows between those services.
Imagine every time we wanted to go and make a change or add a new service or build a new system, add a new dot or add a new edge, we needed to go through a complex change governance process. We'd basically get nothing shipped. This talk is on some of the key investments that we've made to facilitate that ever-increasing rate of change.
To start with, let's start with the anatomy of what a microservice looks like. This is our developer experience contract to engineers that work at Monzo. A key principle that we've had ever since we've gotten started is uniformity in how we write our services.
For the vast majority of microservices, we build and follow the same template and use the same underlying abstractions. Our platform and security teams deal with the infrastructure and then also embed the best practices into our service layer within these libraries. Within a microservice itself, the engineers filling the top part, inserting their code in that little thin envelope, this is where they add their business logic.
Engineers are not rewriting core abstractions like HTTP handlers and consuming from a Kafka queue, or metrics, or marshalling of data. They can rely on a well-defined, well-typed, well-tested set of libraries and tooling. All of these shared library layers provide batteries-included telemetry by default.
Especially in a microservices architecture, one of the key questions is figuring out the murder mystery. If you've got communication flows happening between services, how do you figure out what went wrong and where the data flows happened? In order to do that, you need to have really good telemetry.
We embed that right at our library layer to have that across all of our services, whether your service is serving one request or 1 million requests per second.
We've been quite fortunate. Our platform stack is relatively modern. We spent some really early innovation tokens in 2016 betting on technologies like Go and Kubernetes and microservices as part of our foundations.
Since then, we've been very deliberate in adding to that stack and introducing new technologies and integrations. A key principle for us is that we don't expose these platform-level components to engineers directly. Instead, we build Monzo-specific library abstractions on top, which provide an easy-to-use abstraction that is well-tested and well-maintained and especially has that telemetry built in.
These foundation bets have massively paid off. We have built and scaled to over 3,000 microservices running in production, and are shipping changes hundreds of times per day. We've only got a couple hundred engineers, and we're shipping many hundreds of times a day.
Engineers don't need to understand the semantics of Kubernetes or YAML or complex Kafka library logic in order to write services and get stuff shipped.
They want to have key primitives and key abstractions that work really reliably and durably. We maintain a monorepo of all of our services, and in that monorepo is a bootstrap generator which outputs a consistent folder structure for every service. As I mentioned, the vast majority of our microservices are written in Go.
This reduces the cognitive overhead of engineers. Whether you've been around for a long time or just joined, you learn the muscle memory of navigating a service. All services look broadly the same.
They follow the same folder structure, and you expect your database logic to be in the dao folder. You expect your queue consumer logic to be in the consumer folder. You expect your RPC layer to be in the handler folder.
You know where these bits of logic are. We follow the exact same software stack across the board.
A key goal for us ever since the early days is to keep that iteration time really rapid. When you generate a microservice, the first thing you do is fill in an API spec in a proto file. We've written generators on top of those proto files to compile the code into RPCs behind the scenes that embed with our service HTTP libraries and provide that batteries-included telemetry.
This definition can include things like validation, which is auto-generated beyond the standard data types that you get in protobuf. This generates a whole bunch of Golang code behind the scenes so that engineers are not handcrafting HTTP requests and unmarshalling and stubbing of data into Go types. Testing and mocking becomes far easier because you're working with static types.
We're using proto files to generate much more than RPC code. For example, we also generate queue handling logic. Queue handling, same basic principle.
You're ferrying data over some transport bus, so you can define that as a type. We will generate all of the communication primitives behind the scenes. Engineers can specify an event payload definition as well as all of the validation criteria similar to RPCs.
Again, we have a proto plugin that will go and generate the Kafka consumer and producer logic calling into our libraries. Engineers can work with these strongly typed objects and auto-generated function signatures and not have to worry about that underlying transport. One big benefit that we get as an infrastructure team and for our security teams is that if you have any bug fixes or any new features or performance improvements, these can be applied across the board.
We just regenerate your service stub and you get those benefits for free.
You're going to see this as a bit of a theme. We've spent a lot of time building and operating our own tools from scratch and leveraging open-source tools as well with a deep integration in mind to fit within our ecosystems. Many of these tools are built with modularity in mind.
We want to expose the features that these tools and packages have to offer, but in quite an opinionated and batteries-included manner, rather than just throwing tools across the wall and seeing what sticks. Our goal is to spend our time and energy getting engineers and making it easier for engineers to do the right thing, to get them onto the paved path. Make it so compelling, make it easy and fast and secure by default, that they choose to build for software on the paved road.
Then we make it really easy to release that software with confidence.
This is how I believe you build a really good developer experience, when you hyper-focus on helping engineers build the right thing well, and then getting that code to production and release quickly and safely and iteratively. When an engineer joins, and in order to get permission to work on our backend and go and deploy code to production, we have a very structured backend 101 guide, which takes you through all of the tools that we have and all of these primitives. Once you've learned it once, it is something that will stick with you throughout your entire career at Monzo.
We take you through all the tools that we have, how we write systems, our opinions on writing Go, our logic, how we structure code, our microservices architecture, and much more. One of the big benefits is that we're able to take this documentation, all the standardization that we have in mind, and encode it as skills to put into LLM tools.
These LLM tools are not hallucinating service packages and service structure. They're able to go and navigate code similar to how an engineer would go and navigate code. We could just surface this documentation directly because we've spent the time and energy standardizing all of our tools and all of our processes. Documenting it for humans, and now we could put it into augmenting engineers as well.
Coming back to that service anatomy, automated tools like Claude and Cursor learn our processes for writing code just like humans do and conform to the service structure. Another big benefit is that we have 3,000-plus examples in a monorepo that you can go and reference. These AI tools are constantly learning about our consistent patterns and are able to go and look at implemented examples behind the scenes.
They're not just hallucinating random stuff out of thin air. In the end, each service is a couple thousand lines of actual business logic. That size is really understandable for a group of engineers.
You can put out a PR, and it's something that is very easy to understand. It's also really easily referenceable within a context window. Quite a few distinct services get involved in real time when you make a transaction on our platform.
For example, when you tap your Monzo card or any payment card, many services get involved in that flow in order to make a decision.
We have a very small time budget in order to respond to whether we accept or deny that particular payment. Things like checking if you have the right amount of money in your account and whether this is fraud, making sure that we're protecting against financial crime and a bunch of other areas that we need to categorize for a particular payment to be accepted. A key challenge that we have is that engineers are constantly making changes to the system to adapt to the rapidly changing environment.
Engineers want to be able to test these flows end-to-end. We have a strong incentive to encourage them to do so. Fundamentally, we are a bank.
A key principle of a bank is accepting payments and handling payments. That is something that we want to continuously test end-to-end. We want engineers to have confidence in the changes that they are shipping.
Historically, we've not had a great local development experience on getting the full stack up and running.
Imagine, in order to change something in this flow, you had to spin up 250 microservices, each with their own dependencies and configuration and a bunch of other things. Then, on top of that, you've got machine learning models that are running as part of this pipeline as well. Machine learning models, then you're setting up a Python environment, and you're praying to the Python gods, let this be the pyenv that works for me and this version of pip and getting all of your dependencies in order.
It's a bit of a nightmare. Having that happen across a broad spectrum of engineers is something that is really complex to manage. Imagine we want to scale that to a group of non-engineers as well.
Imagine asking a PM to go and install Python, and go and pray to the Python gods and make a sacrifice and hopefully they can get the machine learning model up and running. They're going to have severe friction and that's something that we want to be able to go and solve for.
There was a really good presentation a couple years back from Soam, where he was representing Stripe, and they talked about how they built around this problem. Stripe would provision remote instances that would get allocated per engineer. An engineer could have one or more instances.
They built tooling to automatically sync code from their local machine into this dev box. Effectively, this was an instance that would run in the cloud and it'd check out a bunch of the code, and you can go and spin up the entire Stripe stack, or a subset of the Stripe stack onto this machine. They significantly worked on the user experience to make it feel like you were developing on your local machine.
It felt like you were running and testing the change locally. We wanted to mimic this experience. We wanted to allow engineers to go and develop and iterate locally really quickly, but then execute the actual logic when it came to doing the full end-to-end test in a remote environment.
We started with a very core premise. If you want to go and make a change in a subset of services, you shouldn't need to run the entire chain of services to test the change end-to-end. Many of these services I mentioned will have their own data model and third-party dependencies and many other things.
We didn't want to put all of that friction on top of our engineers. We had a few options on the table. If you're looking at it from a first principles point of view, one idea is to lean really heavily into mocking and test contracts, and recorded outputs and mocking across service boundaries.
That emulation only gets you so far. We found the safest approach is to actually go run the code itself, rather than trying to emulate or mask its side effects. We have a perfectly functional staging environment that is running a copy of all of our services and all the integration is taken care of for you.
We run our entire platform stack on top of this staging environment. It's fully configured and fully operational. We've integrated it into, for example, a test rack that speaks directly to MasterCard and a bunch of other areas.
Naturally, what we had is we had engineers converge on mostly testing their changes onto the staging environment and to test their changes end-to-end. Engineers would effectively go and deploy from a branch, and go and deploy that onto staging. As we added more engineers, we encountered numerous services under contention.
For example, if you're changing something within the account service, we only have one account service running. Engineers would be contending to go and make changes into that account service. Multiple engineers wanted to go and run their code.
We needed to come up with a way so that numerous engineers could run the same service without having to run the entire platform and not disrupting the entire shared global namespace.
Just a quick primer on how our staging environment works. When a request comes into our staging environment, we route the request just over standard HTTP to the service running within the same staging namespace. For example, let's say that you've got the pink service on the left.
It wants to go and talk to the Aqua service at the bottom. It may have dependencies within that call path on a bunch of other services. It'll go ping-pong some requests and send the response right back.
What we did is we built a layer where engineers can spin up their own individual namespace within their staging environment and deploy only the services that they have changed. We call this system tenancies. It means that an engineer doesn't need to run all of their services that are part of the end-to-end request chain that they are testing.
They don't need to know about all the 250 services and get them running in a completely separate environment.
That means they're able to test their scope changes end-to-end. For example, if they have a service that is being depended on as part of that path, it goes and calls into the shared namespace. We also balance not spinning up dedicated infrastructure per engineer.
An engineer can have one or more tenancies running at a time. These tenancies are namespaces in our staging environment, so they are really quick to provision. For example, if you've worked with Kubernetes, we've got a Kubernetes namespace.
We also expand onto that with other infrastructure abstractions as well. A Kubernetes namespace is very easy to create and very easy to destroy. These are namespaces in Kubernetes and in Cassandra and Kafka.
We've got intelligent handling of this at our library layer. When a request comes in and is routed via a tenancy, we intelligently route these services that are not running in the tenancy back to the same shared staging namespace.
This is bidirectional. A service in the shared namespace can call into the tenancy if it needs to route outwards. The method to enable this isn't very sophisticated. What we do effectively is add a header. We add a header to every request, similar to how you'd add a header for OpenTelemetry, or any sort of thing that is passing across request boundaries. We add a header to say, this is running as part of a tenancy, so you may need to do something a bit funky with your request routing. We interject all of these requests routing config at our service mesh, and we do this intelligent routing.
This works really well for stateless requests. We also want to avoid cross-contaminating of data. Example, let's say that you've got a service deployed in this shared namespace, but you generate some garbage data.
It might look valid for the services that are accepting it and pass all the validation checks. For example, it's still garbage when you think about it from a logical point of view. That should not permanently contaminate the queue and datastores of the shared service.
What we did is we implemented handling of this at our shared library layer. Each of our shared components supports tenancies. They understand what it means to handle a request that comes from a tenancy.
For example, at our Cassandra layer, that might mean segmenting that data away into its own container. For our Kafka layer, that means routing the Kafka message into its own topic and queue, and not polluting the main topic.
That means that you don't end up with a queue message generated in a tenancy, which might be valid but illogical that blocks up a queue because the code to handle it isn't in the global namespace. As I mentioned, since these tenancies are really lightweight and can be provisioned in minutes, we can have hundreds and thousands of them running simultaneously. When a tenancy is no longer needed, we can spin it down equally as quickly.
This paradigm has worked extremely well to solve for this engineer contention problem because we can now provision all of these tenancies for all of our engineers, and engineers can have numerous tenancies on the go. We've been able to extend on this paradigm to also enable this for acceptance tests. Historically, we had acceptance tests running on our staging environment.
These acceptance tests would need to be scheduled sequentially and would be flaky if an engineer deployed a change midway to a service that was part of the acceptance test, maybe with some bad code. We increased confidence in these tests, and now we run these acceptance tests as part of a pull request, because we're able to deploy this acceptance test logic into a tenancy itself.
Once you've got all of your changes lined up, you can go and submit a pull request and go seek a review from code owners of that particular service. Behind the scenes, while you're going and seeking a human review, we're already running a bunch of CI checks and validating the structure and the logic of your code. Before we make these checks required, we're continuously adding checks.
We're continuously inventing new checks to add based on learnings that we've had. We make sure to clean up our existing codebase so that engineers are not tripped up by failing checks that may result as a result of code that is irrelevant to the change that they were making. That means that these checks are really high-quality signal for our engineers, and engineers don't default to ignoring these checks.
What we've been able to do is add significant friction in bypassing these checks.
For example, you have to escalate to numerous people and effectively have a break glass procedure and justify why you are bypassing these checks. We want to make the paved road the correct behavior, the path of least resistance by mandating all of these checks and having really high signal. I firmly believe that making your checks robust and fast and easy to understand is the biggest areas of technical leverage.
It's not something that you could just buy an off-the-shelf product for. There are some vendors out there that will go in and say, you can make your CI this much better. I think fundamentally you need to embed this as a technical discipline within your organization.
It takes a lot of effort to make CI checks high signal, and it's not something that I think is optional. When an incident occurs because of a code bug, a concrete question that you should be asking is, how can I turn that into a check so that that bug doesn't occur in the future?
If your CI checks are slow or error prone, this is a really fantastic area to work alongside LLM tools to find the bottlenecks and speed them up. Every PR that goes through production requires a human review, so all services have a human code owner, and it's a mandatory reviewer to make sure that a change is well communicated to the owners of the services. We have services changing all of the time.
We have engineers contributing across the board. It's not just that engineers only contribute to services within their team. Engineers are contributing to the entire microservice ecosystem within Monzo, because it's all in one monorepo, and all the service structure looks the same, so you have that element of familiarity.
CI gives you confidence that you have built the thing correctly. We're able to check the logic of the code and make sure you're doing all of your error handling and things like that, which frees up time and energy for our human reviewers and teams to go in and assert whether we've built the correct thing.
These are the two fundamental things. You want to make sure that you've built the correct thing and you're solving for the product requirement that you're trying to solve for, and then you have built the thing correctly. Make sure that all your logic is the same. We spend a significant amount of our time and energy having deterministic tests for whether we have built something correctly so that we can spend our human tokens making sure that we have built the correct thing.
An area that is continuously under development, and an area that gets a lot of attention, is the ability to go and reason and review code. It can definitely be a leg up, but implemented incorrectly, you just get a whole bunch of nits, just stuff that is completely irrelevant, not finding big logic flaws, but just pointing out typos. From my point of view, you don't need a fancy AI code reviewer to go and solve all of your problems.
Instead, spend the time and energy writing really high-quality CI checks that will pay for itself. It is deterministic. When you are using AI-based reviews, make sure they are high signal in actually checking for business logic issues, making sure that you've built for the right product rather than trivial semantic nits.
For us, these CI checks that we have built deeply introspect code to flag incorrect logic behavior or behavior that we want to discourage.
We lean very heavily into the Go AST parser and new packages like analysis to go and build up these checks. What we have is a series of small and independent checks that run really quickly. You can run them locally on your machine to test for patterns that we want to strongly discourage.
You can run them as part of a Git push, as a pre-push hook. For example, we have a check to see whether you're importing cross-service packages that we want to discourage. For example, we want to make sure that you're only communicating and importing code from another package when you're communicating over service boundaries rather than just directly importing its code.
We want to make sure that services are decoupled and don't rely on internal implementation details. This can also apply to building secure services as well. For each of our services, we want to make sure that we have egress and ingress firewall capabilities.
For example, we have an API service. It's a relatively trivial API service that is printing all of our bank branches. I think it's part of some UK regulation that you need to have an API that, in programmatic form, gives you all of your physical bank branches. For us, a very stable API, it gives you the empty list. This service has no need to go and talk to any other service whatsoever to serve an empty list. It should have no need to go and speak to the ledger or any other third-party service to go and serve this particular API request.
Again, remember our microservices graph. We have a lot of services running, and we don't want engineers managing all of these connections by hand. It's very easy to get wrong and open up a vulnerability.
In that branches API, imagine that there was a third-party dependency that was introduced as part of the software stack that communicated to a command and control center. By virtue of having all of this static analysis, we're able to build these firewall capabilities in a really nice manner that is easy for engineers to reason about, that you are explicitly adding a rule to communicate with a third party and immediately block everything else. We generate a series of these rule files that sit alongside your code, enumerating all of the valid bits of egress that you are able to do from your service and check that in alongside your service coders, alongside your pull requests.
These are peer-reviewed and sit alongside that service code. It means that we can add a check in CI to make sure this data also remains consistent on every change, and our security posture remains tight and everyone is happy. For writing these checks, I've usually been a huge proponent of things like Semgrep, because it's really easy to write these static analysis checks. Semgrep also works across a wide variety of programming languages. You can write checks for SQL, you can write checks for Python, and a bunch of other programming languages. You don't need to get into the detail or abstract syntax trees for all of these different languages.
What I found relatively recently is working through these with Claude has been a real enabler to write more native syntax checking tools. It's been an absolute game changer for complex logic testing. For example, let's say that you want to track the lifetime of a variable across functions to make sure that it's not being mutated concurrently.
I highly recommend you give this a go. Especially if you generate a corpus of test cases and examples, you can get an analyzer generated by Claude without hand-rolling a bunch of AST parsing code. It can give you really strong inspiration.
Static analysis is also a really good way to bring a codebase towards a path of consistency, a path of convention. Introducing a new static analysis rule stops further additions of bad code being merged and promoting areas that you want to deprecate from. Then you can track and focus on removing existing use cases.
Again, this is a really high-leverage thing that you can do with LLM tools. If they're able to run these checks across your entire codebase, they're able to identify all of the use cases and then effectively do some of the grunt work to go and raise PRs and fix some of these issues behind the scenes, and make sure that these issues are fixed because you've spent time writing a deterministic check. Rather than nowadays spending a bunch of time hard-coding exceptions, one of the key things, if you want to make sure that you have a green build, is you're spending a bunch of time evaluating all of the exceptions and saying, won't fix, or, go ignore whatever for that particular check. Instead, you can just one-shot it into an LLM and ask it, can you go and fix this? If not, add an ignore tag.
Once a service is out there, you need to gain insight into how it's performing. Especially in the era of LLMs and autonomous systems churning out code, it's very easy to deploy buggy or bad-performing code. We use a lot of open-source tools like Prometheus and Grafana and OpenTelemetry to run our observability stack.
We invest very heavily into scraping and collecting all of this telemetry data from our services and from our infrastructure. It's not cheap. We pay a lot of money for all of this.
We are thankful every single time we have an incident, and we can identify the contributive factors because we have all of this telemetry. For every service that comes online using our batteries-included libraries and tooling, we get thousands and tens of thousands of data points, which we have plotted onto a generic dashboard. Again, we can give access to this to LLM tools to go and help debug our systems during incidents.
This also feeds into alerts. We have automated alerts for all of our services based on these common metrics, which we can track back to a change being deployed. Alerts are automatically routed to the right team that owns that particular service.
For most backend systems, performance profiling is usually locked away for various reasons. Special privileges, you need root access onto the machine and things like that. Very few organizations are comfortable with giving engineers the ability to go and run profilers in production.
This attitude really needs to change. You need to have capabilities to go and introspect your systems. Many languages now provide out-of-the-box profilers that are really performant that basically add zero or very negligible overhead.
We make it really easy for engineers to go and invoke the Go profiler into our backend systems and provide a nice web UI to go and request these profiles. Java has really good tools using Flight Recorder. If you're using Python, py-spy is also really good.
Really low overhead.
I've talked about making developer tools really fast. I've been doing a lot of work in the data engineering space lately. Most of these tools are written in Python.
It's really easy to write inefficient Python code. Copying lots of objects, casting something between a map and a list. You get this illusion in front of you that by having this one function call, it will be relatively cheap.
Then you have a million operations that are being done in a DataFrame. This very cheap logic has become very time consuming. A simple native implementation would have much less overhead.
There are plenty of opportunities to find improvement and optimization. Shaving a couple seconds or a couple tens of seconds or a couple minutes out of a developer tool has a meaningful improvement. There are also tools out there like Pyroscope, which allow you to run continuous profiling.
Software is being continuously profiled so you can spot if there's a change that has made a regression in performance.
Putting it in a language that business folks will understand, the faster your code runs, the less servers you need, the less your cloud compute bill is or your infrastructure bill is. To incentivize this with engineers, we have a Slack channel called Graph Trending Downwards. Engineers can shout about their optimizations.
They're able to broadcast to the world. We have all of our engineering leaders and our CTO and engineering directors routinely lurk in the channel and celebrate these optimizations, something that you can brag about when it comes to review season. This is a net benefit for the entire engineering discipline.
It's an activity that is rewarded in personal reviews and feedback. Again, this is a really fantastic way to leverage autonomous coding. Rather than giving it a large program and saying, find all the ways that you can go and optimize, blindly, you can give it a CPU or a memory heap profile and access to the code.
It can target optimizations to where it's found hotspots in your code. It can, for example, reduce the amount of calls to an expensive function, or go and optimize the expensive function right from the get-go. I know there's teams that have adopted LLMs for bug tracking. For example, you might have just hooked in LLMs in Sentry or some other tools that you might be using. For example, where you can have a button that says, you've had an exception, go and try and fix it by giving it access to your code. You could do the same with performance profiling, and your engineers will be much happier and your financial folks will also be much happier.
Lastly, let's talk about debugging systems in production. It's important for engineers to be able to go and debug the business logic. You're able to debug everything else.
Why are you not able to debug your business logic? Sometimes that means querying the service endpoints and seeing what state the data is in, even in production. You can serve this with privacy and sanitization in mind.
I see two extremes with this in many organizations. Either engineers are forced to beg and plead to get access to the production database or to be able to go and call endpoints directly in production under some illusion of security. Or, on the flip side, they have to add lots of loglines and do lots of whack-a-mole with telemetry and deployments to go and see, like, "It was definitely not this.
Definitely this. I definitely hit this function over here," to go and debug their systems.
What we've done is we've built a CLI that allows you to speak directly to production. This securely authorizes into our platform and allows engineers to go and directly interact with our backend services, manage service configuration, schedule operations, and interact with our infrastructure and much more. It's our opinionated way of exposing bits of functionality that might constitute a chain of requests.
For example, configuration management. This is all handled via the CLI. We've very recently added really strong validation into our config management to make sure that engineers are not putting illogical changes into our config system.
We've also embedded things like peer reviews. For example, we built our own multi-party authorization system that allows you to have a second pair of eyes to review changes. Or, for example, if you're calling a sensitive RPC, you need to get approval from the code-owning team to make sure that you're not making bad changes in production.
I see many power users proud of their command line history, adaptable shell commands. You might have this really funky Postman script or this fantastic curl command. However, when you build this as a tool, everyone can contribute and everyone benefits. One of the biggest net benefits that we see is that we have non-engineering folks who use the Pound CLI on a day-to-day basis as well. These are folks that are not going to be comfortable with having just a big IPython Notebook or a Postman script.
A lot of the tools that I've shown are things for backend and frontend engineers. You can also automate things in spaces like machine learning and mobile testing as well. For example, we have a whole heap of tooling that generates ML service code and gets it ready for training and testing on top of Vertex AI, and then takes a model and then builds it on top of our platform and deploys it.
For example, what I'm showing on the screen, we have a CLI tool that interacts with the Android emulator to help automate tasks that an Android engineer would be doing on a day-to-day basis. Especially if you're an Android engineer and you're doing some form of release testing or you're responsible for doing the QA for that week's release, this stuff is a godsend to go and take automated screenshots and do comparing and run automated tests and spin up the emulator, and things like that.
We have solved for this across the industry and it's very great to see that CLIs are making a comeback. Hopefully this leads to more investment in CLIs. This analogy also extends into enabling changes across a broader group of people beyond engineers.
We want engineers and non-engineering folks to use our paved road. Our tools need to be fast and reliable and well understood. Even for LLMs, calling out to deterministic tools leads to a much more reliable result.
Think about all the provisioning steps an engineer might have to do and compiling when working on a change. If engineers need to do a whole bunch of gymnastics to unblock their developer environment and pray to the Python or Ruby or Node gods, install lots of random packages and wait for minutes and hours for their software to compile, people will give up and they will try and circumvent your paved road. If it's far easier to go sign up to a third-party platform and ship to Vercel or some other system, then they will go and do that instead.
They will find a way. Then, effectively, you have just enabled more and more vibe coding.
To summarize and package this all up, I've shown by example some of the tools that we have built, and these have been investments over the past decade to encode our organizational processes. Take inspiration from what we've built and find places where your organizational processes are falling down, and find impact in automating them and making them easier. A lot of companies are making significant investments in broader accessibility of LLMs, and effectively, you need to have the foundation in order to be able to go and enable that.
The barrier to investing in these tools have become significantly reduced and continues to become easier. Investment in those key engineering capabilities, like having deterministic code generators, standardized package management, consistent test tooling, reliable telemetry, really reliable staging environments, and profiling have a huge payoff in the world where we are enabling humans and LLMs and beyond, where the cost of generating code has become significantly cheaper.
We want to facilitate that rapid iteration, and LLMs can augment engineers if all of these other things are taken care of to validate the hypothesis quicker and safer rather than flying blind. Standardizing on a small set of technology choices and continuously improving on these abstractions, we enable engineers and LLMs to go and focus on the business problem at hand rather than recreating underlying infrastructure every single time we use them. Of course, we don't build everything from scratch.
We are relying on the foundation of the entire engineering ecosystem that we have around us with open source. We rely on a lot of open source and a lot of third-party services too, but we don't directly project the open-source technology into our microservices and system. We have a very opinionated approach.
We have been very intentional. Be intentional in the experience that you want engineers to have and then project the interface to match that experience.
Participant 1: Question number one, with having such an opinionated stack and all the libraries built, doesn't that kill developer engagement because they're just solving the business logic, which is not always the most interesting part for a developer. Then related to that, when you come to more low-level high performance, high throughput problems that you need to solve, if they don't understand the libraries that are underneath them, how can they debug and investigate and tune those sorts of things?
Suhail Patel: Let's talk about the motivation for most engineers. I think this is an illusion that we have built up across the industry. Engineers want to go and solve complex engineering problems, and there is plenty of complex engineering problems within the domains that they focus on. For example, if you're in the domain of payments or financial crime, there's enough interesting work in tuning your ML models and coming up with more and more sophisticated rules, or making sure that we are processing payments more reliably to keep those engineers engaged and to keep their mind engaged. It doesn't necessarily need to be at the infrastructure level.
The second question around high throughput and high understanding. We spend a significant amount of time documenting and explaining the constraints within our systems. Our systems are not perfect.
For example, we communicate our RPC stack over HTTP. HTTP has its limitations, of course. You might be relying on a particular implementation of HTTP.
You might ask, why not gRPC, or insert some protocol over here? What we've relied on is a very strong set of well-tested and well-opinionated frameworks that we have proven to be able to deploy and scale significantly to a large amount of numbers. We have found very few use cases where engineers need to go and breach those throughput limits.
Fundamentally, the question that we ask engineers is that, why do you believe that you need to go and go outside of that paved road? Because we've been able to prove it at this element of scale, which usually is far beyond the scale that they are trying to achieve. It becomes from just a thought hypothetical exercise to actual reality exercise, which is, on the ground, we have systems that are doing millions of requests per second with the constraints that we have built.
Why are you not able to build your system within that container?