/de/case-studies / More Than Control Tower
// Case Study · Produkt / Supply-Chain-Plattform

More Than Control Tower — die modulare Plattform für Produktions- und Distributionsunternehmen

Ein fertiges Produkt für das Supply-Chain-Management — an den konkreten Betrieb des Kunden angepasst. More Than Control Tower (MTCT) ist mehr als ein Leitstand: 8 Bounded Contexts (Lager, Vertrieb, Spedition, CRM, HR, Buchhaltung, System, Auth), konzipiert als Plug-and-play — jeder läuft eigenständig oder ist darauf ausgelegt, sich an ein beim Kunden bereits vorhandenes System anzubinden (Fakturownia, externes WMS, ERP, HR). 26 dokumentierte Architekturentscheidungen, 600+ Tests, ESM-nativer Stack: TypeScript 6 + NestJS 11 + PostgreSQL 18.
Kunde · Pro Deployment angepasst
Zusammenarbeit · Produkt + Customizing
Rolle · Volle Produkt-Eigenverantwortung
Sektor · Supply Chain / B2B
8
Bounded Contexts
26
Dokumentierte Entscheidungen (ADR)
600+
Tests (Unit + BDD + Integration)
0
Vendor-Lock-ins
01 · Kontext

Das Produkt und sein Marktkontext#

Ein typischer Kunde aus Produktion und Distribution nutzt bereits Fakturownia für Rechnungen, hat ein eigenes WMS vom Regalanbieter, importiert CSV aus dem TMS des Spediteurs und führt HR in Excel. Was fehlt, ist die Schicht, die das alles zusammenführt und einen einzigen operativen Überblick liefert — wer was produziert, wo es liegt, wohin es geht, wer dafür bezahlt hat. MTCT ist diese Schicht. Mit der Option auf einen vollständig nativen Stack für Unternehmen, die bei null anfangen.
Zielgruppe
KMU Produktion + Distribution
Unternehmen, die Waren produzieren und ausliefern
Vertragsmodell
Produkt + Customizing pro Projekt
Deployment-Modell
On-Prem oder Managed · Single-Tenant
Modularität
8 Bounded Contexts · jeder austauschbar
Eigentum
philosopht — volle Roadmap-Kontrolle
Produktstatus
Kern fertig · Adapter pro Kunde
Produktziel

Eine modulare B2B-Plattform für das Supply-Chain-Management liefern — eine, bei der sich jedes Modul herauslösen und an ein beim Kunden bestehendes System anbinden lässt. Statt ein „Rip and Replace" zu erzwingen, bindet MTCT ein, was der Kunde bereits hat — Fakturownia für Rechnungen, externes WMS, ERP, HR — und schließt mit nativen Modulen die Lücken dort, wo sie fehlen. Architektur: hexagonal + CQRS + Domain Events, jede externe Integration hinter einem Port, jedes Modul auf drei Ebenen getestet (Unit, BDD, Integration mit Testcontainers).

02 · Herausforderungen

Was wir lösen mussten#

Eine multimodulare Plattform ergibt nur dann Sinn, wenn ihre Module wirklich austauschbar sind — und nicht erst dann, wenn „wir sagen, dass sie es sind". Das verlangte frühe, disziplinierende Architekturentscheidungen. Jeder der zwölf Punkte unten ist eine konkrete Herausforderung, auf die wir eine Antwort brauchten, bevor wir den ersten Controller schrieben.

Plug-and-play-Module

Jeder der 8 Bounded Contexts muss eigenständig laufen oder sich an das externe System des Kunden anbinden. Keine Magie — Ports und Behavioral Contracts.

Kommunikation ohne direkte Imports

Module importieren sich nie gegenseitig. Sie kommunizieren über QueryBus (read), CommandBus (write) und Domain Events (Reaktivität). Grenzen sind erzwungen, nicht vereinbart.

Modularer Monolith mit Microservices-Plan

CQRS-Busse sind eine Transportschicht. Microservices-Migration = In-Memory-Bus gegen HTTP/gRPC oder RabbitMQ tauschen. Kein Domain-Rewrite.

Datenkonsistenz

ACID innerhalb eines Aggregats (ein Modul, eine Transaktion). Eventual Consistency zwischen Modulen — Domain Events propagieren Änderungen asynchron.

Durchgängige Typsicherheit end-to-end

OpenAPI → Kubb → TanStack Query Hooks. Eine Änderung im NestJS-Controller wird zum Compile-Error in React, nicht zum stillen Runtime-Crash.

Domain Driven Design in der Praxis

Aggregate mit Methoden, Value Objects (Money, GoodDimensions), Domain Errors. DDD als Design-Disziplin, nicht nur als Ordnername.

Tests auf drei Ebenen

Unit (Domain-Logik, ~513 Tests) + BDD-Handler (CQRS mit In-Memory-Repos) + Integration (Testcontainers mit echtem PostgreSQL).

Autorisierung als Port

Module wissen nichts von Rollen. Sie fragen AuthorizationPort.canDo(action, user). Migration auf Keycloak / Auth0 = Austausch einer einzigen Implementierung.

Integrationen mit Kundensystemen

Fakturownia.pl, externes WMS, ERP, HR, TMS — jede Integration hinter einem Port mit Behavioral Contract. Den Adapter schreiben wir, wenn wir die echte API kennen.

CLI für Operatoren

nest-commander teilt sich die CQRS-Busse mit HTTP — eine Logik, zwei Einstiegspunkte. Der Operator macht im Terminal dasselbe wie der Nutzer im UI.

ESM-natives TypeScript 6

Gesamter Stack auf ESM, nodenext-Resolution, keine CJS-only-Abhängigkeiten. Ein Stack, der für die nächsten 5 Jahre bereit ist, statt technischer Schulden ab Tag eins.

Mehrsprachiges UI ab Tag eins

Inlang Paraglide — kompilierte Übersetzungen, typsichere Keys, null Runtime-Overhead. PL und EN sind First-Class-Citizens, kein Nachgedanke.

"
Tests sind keine optionale Nacharbeit — sie sind Teil des Features. Ohne sie gibt es kein „fertig".
MJ
Michał Jeziorski

Ein ähnlich komplexes Projekt? Sprechen wir darüber.

Module, Rollen, Integrationen in bestehende Systeme und Skalierung — wir wissen, wie daraus ein stimmiges Produkt wird.

03 · Vorgehen

Wie wir den Produktkern bauen #

Drei Phasen, in denen ein MTCT-Modul entsteht. Dasselbe Muster für alle acht Bounded Contexts — Vorhersehbarkeit ist ein Produktmerkmal, keine Bürokratie.

01

Bounded Context und Verträge

Wir schneiden das Modul mit eigener Domänensprache, eigenen Ports und eigenen Tabellen heraus (Präfix `warehouse_`, `sales_`, …). Wir entscheiden, was wir als Domain Event veröffentlichen und was intern bleibt. Keine physischen Foreign Keys zwischen Modulen — logische Referenzen (UUID).

02

Domain-First-Implementierung

Aggregate mit Methoden, Command Handler, Query Handler, Domain Events. Controller sind eine dünne HTTP-Schicht, das ORM ist ein Infrastrukturdetail. Jeder Test wird zusammen mit dem Feature geschrieben — nie danach.

03

Behavioral Contracts für Adapter

Statt spekulativer Adapter für „irgendwelche" externen Systeme — definieren wir den Port und eine Reihe von Szenarien „was der Adapter können muss". Erster Kunde = erster Adapter, aber Interface und Tests stehen bereit.

Volle technische Eigenverantwortung

Ein quelloffener, ESM-nativer Stack: PostgreSQL, NestJS, MikroORM, Vitest. Null Vendor-Lock-ins. Code bereit für Audit, für die Übergabe an das Team des Kunden, für die Migration auf eigene Infrastruktur. Architektur vorbereitet für die Extraktion in Microservices, ohne die Domäne neu zu schreiben.

04 · Screens

Wie das Produkt in der Praxis aussieht#

Acht Ansichten aus der Referenzinstanz von MTCT — vom operativen Zentrum bis zum Rechnungsmodul. In einer realen Einführung passen wir Layout, Farbe und Modulumfang an Branding und Bedarf des Kunden an.

Die Screens stammen aus der Referenzinstanz von MTCT mit geseedeten Demodaten. In einer realen Einführung passen wir Farbe, Layout und Modulumfang an Branding und Bedürfnisse des Kunden an.

05 · Ergebnisse

Was in der Box steckt#

Zahlen aus dem aktuellen MTCT-Kern. Jede ist die Konsequenz einer konkreten Architekturentscheidung, kein Nebeneffekt.

8
Bounded Contexts (Lager, Vertrieb, Spedition, CRM, ERP, HR, System, Auth)
26
dokumentierte ADRs — jeder mit Konsequenzen und Alternativen
600+
Tests insgesamt (~513 Unit + BDD-Handler + Integration mit Testcontainers)
100%
typsicher von OpenAPI bis zu den React-TanStack-Query-Hooks (Kubb)
0
Vendor-Lock-ins — quelloffener Stack, jedes Modul austauschbar
3.8×
Vitest schneller als Jest — gemessen bei der Migration in ADR-011
Ein ähnliches Projekt in Aussicht?Sehen Sie, wie wir andere komplexe Projekte mit vielen Nutzern und Rollen umgesetzt haben.
Ab hier — Technical Deep Dive
Unten — Architektur, Stack und Umgebungen: drei technische Entscheidungen, die das Produkt am stärksten prägen, plus eine vollständige Aufschlüsselung der Bibliotheken und Einführungswege.
06 · Architektur und technisches Vorgehen

Drei Entscheidungen, die das Produkt definieren#

Plug-and-play kommt nicht aus dem Marketing. Es kommt aus drei konkreten Architekturentscheidungen, getroffen bevor wir das erste Modul schrieben. Jede hat ihren eigenen ADR — mit Begründung, Konsequenzen und verworfenen Alternativen.

Entscheidung 01 · Kommunikation zwischen Modulen

CQRS-Bus als einziger Vertrag zwischen Bounded Contexts

Ein modularer Monolith, in dem sich Module gegenseitig importieren, ist ein Monolith, der vorgibt, mehr zu sein. Der erste Lazy Import bricht die Isolation, der zweite erzeugt eine zyklische Abhängigkeit, der dritte macht aus dem „austauschbaren Modul" eine Fiktion.

In MTCT importieren sich Module nie gegenseitig. Sie kommunizieren ausschließlich über drei Busse: QueryBus (modulübergreifende Reads — z. B. fragt die Spedition das CRM nach den Verfügbarkeitszeiten des Kunden), CommandBus (Writes — eine Änderung in einem anderen Modul erzwingen), EventBus (Reaktivität — der Vertrieb publiziert OrderPlaced, das Lager reserviert die Ware, die Spedition reiht sie in die Planung ein).

Konsequenz: die Migration auf Microservices ist ein Austausch der Bus-Implementierung, kein Domain-Rewrite. In-Memory-Bus → HTTP/gRPC für Reads, RabbitMQ/Kafka für Events. Domäne, Handler und Aggregate bleiben unverändert. Das ist kein Versprechen — es ist eine architektonische Garantie, erzwungen durch das Verbot direkter Imports.

Entscheidung 02 · Adapter für externe Systeme

Standalone-first — den Adapter schreiben wir, wenn die echte API bekannt ist

Integrations-Frameworks locken mit „universellen Adaptern" für gängige Systeme — WMS, ERP, Rechnungswesen. Klingt pragmatisch, in der Praxis aber: Jede solche Integration beruht darauf zu raten, wie die API „aussehen sollte". Der erste echte Kunde mit System v3 statt v4 = ein Rewrite von Grund auf.

In MTCT sitzt jedes externe System hinter einem Port (z. B. InvoicePort, WarehouseSyncPort). Bevor wir einen Kunden haben, definieren wir nur das Interface und einen Behavioral Test Contract — eine Reihe von Szenarien „was der Adapter können muss". Den Adapter selbst schreiben wir erst, wenn wir die konkrete API des Kunden kennen.

Konsequenz: null spekulativer Code, null tote Integrationen. Der Kern ist fertig, Adapter schreiben wir pro Einführung dazu — und jeder ist ab dem ersten Tag eine präzise Passung an die echte API, kein Kompromiss.

Entscheidung 03 · Autorisierung und Identität

Autorisierung als Port — Module wissen nichts von Rollen

Kunden haben unterschiedliche Autorisierungsanforderungen. Einem kleinen Betrieb genügen Rollen in der Datenbank. Ein mittelständisches Unternehmen will LDAP. Ein Konzern verlangt SSO über Keycloak oder Azure AD. Liegt die Autorisierungslogik über die Module verstreut, ist jede dieser Varianten ein Rewrite von acht Bounded Contexts.

In MTCT prüfen Module nie direkt Rollen. Sie fragen AuthorizationPort.canDo(action, user, context). Mehr noch — das Modul System (Identität: E-Mail, Name, Rollen) ist physisch vom Modul Auth (Credentials: passwordHash, Refresh Tokens) getrennt. Das erlaubt, das eine auszutauschen, ohne das andere anzufassen.

Konsequenz: die Migration von lokalem RBAC auf Keycloak = das Schreiben eines einzigen KeycloakAuthorization-Adapters. Der Rest des Produkts merkt davon nichts. Dasselbe gilt für Auth0, ein eigenes OIDC oder jede neue Lösung, die sich der Kunde ausdenkt.

Tech-Stack#

Die Bibliothekswahl rund um die drei Entscheidungen oben. Standard, gut gepflegt, quelloffen, ESM-nativ. Kein Verwaisungsrisiko, keine Vendor-Lock-ins.

SPRACHE & RUNTIME

TypeScript 6Node.js 22ESM nativenodenext resolution

BACKEND-FRAMEWORK

NestJS 11@nestjs/cqrs@nestjs/event-emitternest-commander

ORM & DATENBANK

MikroORM 7PostgreSQL 18schema-first defineEntitymigrations CLI

VALIDIERUNG

Zod 4class-validatorruntime + compile-time

TESTING

Vitest 4@testcontainers/postgresqlsupertestBDD handlers

AUTH & SICHERHEIT

JWT httpOnly cookiesArgon2Authorization portrefresh token rotation

FRONTEND

ViteReactTanStack QueryTanStack FormInlang ParaglideKy

API & CLIENT

OpenAPI · SwaggerKubb (codegen)TanStack Query hooksshared-types pkg

Umgebungen und Einführungswege#

Vier Umgebungen — vom lokalen Dev bis zur Single-Tenant-Produktion beim Kunden. Je näher am Kunden, desto strenger die Kriterien und desto größer der Anteil an Customizing.

01 · DevDevelopment// Zielgruppephilosopht team02 · Pre-testPre-test// ZielgruppeSmallGIS testers03 · TestFlightTestFlight// Zielgruppehunters · PZŁ clubs04 · ProdProduction// Zielgruppeall PZŁ members
07 · Dev Story

Plug-and-play-Module — ein Port und drei Implementierungen #

Engineering-Problem · Autorisierung

Jedes Modul läuft solo. Oder bindet sich an das an, was der Kunde bereits hat.

Plug-and-play klingt wie ein Slogan. Darunter steckt eine sehr konkrete Einschränkung: ein Modul darf nicht wissen, wer die Implementierung liefert. Es darf nicht annehmen, dass Rollen in der Datenbank liegen, es darf kein bestimmtes SDK aufrufen, es darf den Keycloak-Client nicht als Runtime-Dependency installieren.

Die Standardantwort — „alles hinter einem Interface" — reicht nicht. Ein Interface ohne Behavioral Contract ist ein Versprechen ohne Durchsetzung. Der erste Adapter, der in einem unerwarteten Szenario null statt eines leeren Arrays zurückgibt, bricht das ganze Modul — und niemand merkt es bis zur Produktion.

In MTCT entsteht neben jedem Port ein Satz von Verhaltensszenarien — Tests, die jeder Adapter bestehen muss, unabhängig vom Backend (Fake, lokales RBAC, Keycloak, Auth0). Damit wird der Austausch einer Implementierung zur mechanischen Operation, nicht zum Research-Projekt. Der Port definiert das „Was", Contract Tests definieren das „Wie", und der Adapter liefert das „Womit".

// Kern: Module sprechen mit dem Port, nicht mit der Implementierung
interface AuthorizationPort {
  canDo(action: Action, user: UserCtx): Promise<boolean>;
}

// drei austauschbare Implementierungen, ein Behavioral Contract:
class FakeAuthorization      implements AuthorizationPort { /* für Tests */ }
class RoleBasedAuthorization implements AuthorizationPort { /* lokales RBAC */ }
class KeycloakAuthorization  implements AuthorizationPort { /* Kunde mit SSO */ }
Inhaltsverzeichnis
  1. 01Projektkontext
  2. 02Herausforderungen
  3. 03Vorgehen
  4. 04Oberfläche
  5. 05Ergebnisse
  6. 06Architektur & Tech
  7. 07Dev Story
  8. Kontakt
Kontakt

Ein kleiner, klarer nächster Schritt.

Buchen Sie ein 15-minütiges Erstgespräch oder schreiben Sie uns kurz. Auf Deutsch oder Englisch – auf Wunsch mit einem deutschsprachigen Kollegen.

Termin buchen
15-minütiges Erstgespräch buchen
Über Google Calendar – öffnet sich in einem neuen Tab.
Schreiben Sie uns
Adresse
philosopht Dawid Michota
ul. Świętokrzyska 41A
26-001 Wola Kopcowa, Polen
USt-IdNr. PL6573002241
Treffen wir uns
Wir arbeiten remote, treffen uns aber gerne persönlich:
WarschauKrakauKielceLodzKattowitzRzeszówTarnówRadom
In späteren Gesprächsphasen und in der laufenden Zusammenarbeit kommen wir gerne zu Ihnen – in der DACH-Region, im übrigen Europa und in Nordamerika.
Oder senden Sie eine Nachricht
Unverbindlich · Antwort innerhalb von 24 Stunden
Den technischen Versand übernimmt Web3Forms. Datenschutzerklärung