Der Begriff "Architektur" bzw. "Software-Architekt" hat in der Softwareentwicklung nicht den besten Ruf. Man denkt unwillkürlich an umfangreiche Konzepte, fehlenden Pragmatismus, Elfenbeintürme, Architektur-Reviews, merkwürdige UML-Diagramme voller Halbwahrheiten, "upfront design", usw. usf. So ganz ohne Leitlinien geht es aber natürlich auch nicht.
Gerade bei einer Neuentwicklung auf der grünen Wiese hat man also eine faire Chance, es in diese oder jene Richtung komplett zu versauen. In Lhotse haben wir uns im Laufe des Projektes auf folgende Prinzipien geeinigt:
Bei der Makro-Architektur geht es um die "Architektur im Großen": Welche Teilsysteme gibt es, wie arbeiten sie zusammen und an welche Prinzipien müssen sich alle Teilsysteme und Teams halten.
Eine "klassische" Architektur teilt sich in Schichten und Module.
Häufig spiegelt sich dass dann auch in der Team-Struktur wieder: Ein oder wenige Teams pro Layer. Diese Aufteilung ist nicht ganz unproblematisch:
Man könnte noch lange so weitermachen, aber hier geht es um die Lhotse-Architektur: und da haben wir uns für einen vertikalen Schnitt durch das System entschieden.
Jedes grüne Kästchen ist eine eigenständige Anwendung mit eigener Datenhaltung und eigenem Frontend. Manche Teams haben mehr als eine "Vertikale", aber keine Vertikale wird von mehreren Teams entwickelt.
Eigene Datenhaltung bedeutet dabei auch, dass sich Vertikalen keinen Zustand über die selbe Datenbank teilen dürfen. Eine geteilte Datenbank würde wieder eine enge Kopplung zwischen den Systemen erzeugen: sie müssten sich über das Schema der Daten einig sein.
Der interne Aufbau einer Vertikalen liegt vollständig in der Verantwortung des Teams. Insbesondere gibt es keine gemeinsame Code-Base. Das führt zwar gelegentlich zu Parallelentwicklung, dafür müssen aber nicht die abweichenden Bedürfnisse mehrerer Teams in eine gemeinsam genutzte Library gedengelt werden.
Der Begriff "shared nothing" drückt aus, dass sich die Teilsysteme untereinander, aber auch die einzelnen Instanzen einer geclusterten Vertikalen, keinen gemeinsamen Zustand in der Anwendung halten. Es gibt also keine In-Memory Caches, über die sich Cluster-Knoten miteinander unterhalten müssen, keine HTTP-Sessions, die zwischen den Instanzen repliziert werden, usw.
Da sich einzelne Instanzen nichts teilen, müssen sie auch nicht miteinander kommunizieren und kein Load-Balancer muss Rücksicht darauf nehmen, welche "Session" auf welche Knoten geleitet werden muss (das ist u.a. bei Deployments sowie für die Ausfallsicherheit wichtig).
Zustand in jeder Form wird also nur außerhalb des Systems gestattet: Im Browser, der Datenbank, einem Memcache oder in einem HTTP-Cache. Probleme mit der Cache-Coherency werden auf diese Weise vollständig vermieden
Natürlich müssen die einzelnen Vertikalen trotz aller Eigenständigkeit miteinander kommunizieren. Außerdem gibt es den Bedarf, für beispielsweise externe Apps oder ähnliches technische Schnittstellen bereitzustellen. Und dann gibt es natürlich auch noch die Integration der Teilsysteme in eine gemeinsame Shop-GUI und die Konfiguration des Shops durch eine Backoffice-Application (aka "Shopoffice").
Damit alle diese Dinge möglich sind, haben wir uns auf eine REST Architektur geeinigt. REST heißt dabei nicht "wir tauschen XML-Dokumente für HTTP aus und verwenden auch PUT und DELETE"; wir nehmen das etwas ernster:
Unter anderem ist dabei ein Open-Source Projekt http://github.com/otto-de/jsonhome entstanden, mit dem eine Anwendung ein json-home Dokument veröffentlichen oder auch konsumieren kann.
Da es sich bei der Anwendung um einen Online-Shop handelt, stehen die Chancen gut, dass viele Teilsysteme etwas über Produkte wissen müssen. Wenn sie aber keine gemeinsame Datenbank verwenden, wie werden dann Informationen geteilt? Dafür haben wir zwei Varianten:
Die Datenversorgung erfolgt dabei stets asynchron im Hintergrund. Kein Kunde soll auf derartige Prozesse warten müssen. Inkonsistenzen zwischen den Systemen werden dabei bewusst in Kauf genommen.
Die Zugriffe zwischen den Systemen erfolgen dabei grundsätzlich über PULL-Mechanismen wie z.B. AtomPub Feeds da sich auf diese Weise die Kopplung zwischen den Systemen reduzieren lässt. Da wir ohnehin mit Inkonsistenzen rechnen müssen, kommt es auch nicht auf ein paar Sekunden Zeitversatz gegenüber einer Aktualisierung per PUSH an.
Sind solche Inkonsistenzen nicht akzeptabel, dürfen Systeme ausnahmsweise auch Ad-Hoc Anfragen an andere Vertikalen stellen; das versuchen wir jedoch zu vermeiden, da es eine enge Kopplung der Systeme mit sich bringt.
Der Kern des Architektur-Prinzips ist jedoch, dass für alle Daten nur eine Vertikale führend ist. Alle anderen Systeme greifen nur über REST-Schnittstellen auf die führende Vertikale zu und halten sich bei Bedarf redundante Daten. Die "Wahrheit" über ein Datum liegt in der Hand des führenden Systems.
Im Gegensatz zur Makro-Architektur geht es bei der Mikro-Architektur um die "Architektur im Kleinen", also die der einzelnen Vertikalen.
Da die Hoheit über die Vertikalen in der Verantwortung der zuständigen Teams liegen, gibt es hier keine übergreifenden Richtlinien. Es ist nicht vorgeschrieben, welche Frameworks verwendet werden müssen oder wie die Struktur der Anwendung auszusehen hat. Nur ein paar flankierende Absprachen.
Wir sehen unsere Kernkompetenz nicht darin, beispielsweise eine eigene Datenbank oder eigene Frontend-Frameworks zu entwickeln. Stattdessen bedienen wir uns hier am Markt und kaufen entweder Dinge ein oder verwenden schlicht ein "Produkt" aus dem Open-Source Umfeld. Was genau als "Core" definiert ist, liegt dagegen wieder in der Entscheidung des Teams.
Zur Zeit verwenden alle Teams die MongoDB als DBMS. Im Prinzip könnte eine Vertikale sich auch für eine andere Persistenzlösung entscheiden. Allerdings würde das betriebliche Aufwände nach sich ziehen, weshalb wir solche Änderungen vermeiden wollen.
Es gibt noch einige weniger weitere Dinge wie Tomcat als Servlet-Container oder auch gemeinsam genutzte Monitoring-Werkzeuge, die zur Zeit in allen Teams ähnlich verwendet werden, grundsätzlich aber als Bestandteil der Mikro-Architektur definiert sind. Die Teams könnten sich also gegen solche Grundlagen entscheiden, sind aber dazu angehalten, vorher in den Ring zu steigen und ihr Vorhaben in "großer Runde" durchzuboxen.
Anfangs haben wir auch noch eine "common" Library entwickelt und teamübergreifend genutzt. Mittlerweile sind wir jedoch zu dem Schluss gekommen, dass die Nachteile einer solchen Bibliothek (hinterrücks werden wieder Abhängigkeiten zu 3rd-Party Libraries eingeführt) dir Vorteile überwiegen.
Was viel besser funktioniert:
"Wenn es Code gibt, den sich Teams teilen wollen, entwickelt es als Open-Source Projekt, veröffentlicht es auf GitHub und behandelt es wie eine 3rd-Party Dependency".
Bisher sind auf diese Weise drei GitHub Projekte http://github.com/otto-de entstanden:
Zwei weitere sind in Vorbereitung:
We have received your feedback.