07 — Softwareentwicklung

Vom siliziumnahen C zum Edge-TypeScript

Ich wähle die Sprache nach dem Fehlerfall, den ich zu vermeiden versuche.

Rust dort, wo Nebenläufigkeit beweisbar korrekt sein muss, Go dort, wo Netzwerkdienste einfach bleiben müssen, C nah an der Hardware, Python und TypeScript dort, wo Modelle und Menschen auf das System treffen — bewusst gewählt, in der Tiefe betrieben.

Der Nachweis

Das ehrliche Maß eines Ingenieurs ist nicht der Stack auf einem Lebenslauf — es ist das, was den Kontakt mit echten Nutzern überlebt hat. Meines sind elf Jahre davon.

Seit mehr als elf Jahren liefere ich mobile Anwendungen an echte Nutzer auf iOS, Android und Windows aus. Diese Zahl ist der Nachweis, dem ich am meisten vertraue, denn an ein Gerät in der Tasche eines Menschen auszuliefern ist unerbittlich: Es gibt keine Staging-Umgebung für einen Absturz auf dem Telefon eines Fremden, kein Rollback für eine Funktion, die den Nutzer verwirrt. Ein Jahrzehnt davon lehrt eine spezifische Disziplin — Versionsfragmentierung, Offline-Verhalten, Akku- und Speicherbudgets und das langsame Anwachsen des Gespürs dafür, was wegzulassen ist.

Hinter jeder dieser Apps stand ein Backend und die Cloud-Infrastruktur darunter. Die Praxis ist also nicht nur die Vorderseite des Glases; sie reicht vom Touch-Ereignis abwärts, durch die API, die Warteschlange, den Datenspeicher und den Container, bis zum Kernel, auf dem der Container läuft. Ich habe an jeder Schicht genug Zeit verbracht, um zu wissen, wo die Nähte sind, und um so zu entwerfen, dass die Nähte unter Last nicht reißen.

Was ich seitdem hinzugefügt habe, ist Tiefe in Systemsprachen und Multi-Cloud-Auslieferung: Rust und Go für die Kerne, die korrekt und schnell sein müssen, Docker und Kubernetes für die Auslieferung, und eine bewusste Weigerung, irgendetwas davon an einen einzigen Anbieter zu binden. Der Rest dieser Seite ist die konkrete Gestalt davon — Sprachen, Plattformen, Clouds und die kleinen Werkzeuge, die ich mir unterwegs baue.

0+

Jahre Auslieferung mobiler Anwendungen an echte Nutzer

0

mobile Plattformen in Produktion: iOS, Android, Windows

0

Clouds betrieben ohne Anbieterbindung: GCP, Azure, AWS

0

Hauptsprachen in der Tiefe beherrscht: Rust, Go, Python, TypeScript

Der Sprach-Stack

Ein Stack, gewählt nach Schicht, nicht nach Mode.

Oberfläche — Nutzer, Modelle, Daten TypeScript / JS Node, Browser, Edge. Python KI, Daten, wissenschaftliches Rechnen, Automatisierung. Go Nebenläufige Netzwerkdienste. Rust Speichersichere, nebenläufige Kerne. C / C++ Hardwarenah. Hardware — Silizium, Register, Firmware

Schichtdiagramm der Systemsprachen

Jede Sprache verdient ihren Platz auf der Schicht, wo ihre Garantien zählen.

Ich greife nicht zu einer Lieblingssprache und biege jedes Problem zu ihr hin. Das Diagramm liest sich vom Silizium nach oben: C und C++ dort, wo der Code die Hardware berührt, Rust für die speichersicheren nebenläufigen Kerne, Go für die nebenläufige Netzwerkschicht, dann Python und TypeScript an der Oberfläche, wo KI, Daten und Nutzer leben.

Die Balkenbreiten sind ein grobes Gefühl für die Breite der Nutzung über meine Arbeit hinweg — kein Benchmark, nur dort, wo das Volumen an Code tendenziell sitzt.

  • Untere Schichten kaufen Determinismus und Sicherheit
  • Obere Schichten kaufen Geschwindigkeit und Reichweite
  • Die Grenze ist eine bewusste Wahl, pro Projekt getroffen
Ein kleines Stück des Echten

Wie der Code tatsächlich aussieht.

Ein begrenzter Worker-Pool in Go — Arbeit über Goroutines auffächern, Ergebnisse über einen Channel einsammeln und Abbruch respektieren. Das Muster taucht unter fast jedem Netzwerkdienst auf, den ich schreibe.

// FanOut runs fn across at most n goroutines, streaming results
// back on a channel and stopping early if ctx is cancelled.
func FanOut[T, R any](ctx context.Context, in []T, n int, fn func(T) R) []R {
    jobs := make(chan int)
    out := make([]R, len(in))
    var wg sync.WaitGroup

    for w := 0; w < n; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for i := range jobs {
                select {
                case <-ctx.Done():
                    return
                default:
                    out[i] = fn(in[i])
                }
            }
        }()
    }

    for i := range in {
        select {
        case <-ctx.Done():
            close(jobs)
            wg.Wait()
            return out
        case jobs <- i:
        }
    }
    close(jobs)
    wg.Wait()
    return out
}
Vier Bereiche, in der Tiefe

Wohin die Ingenieurarbeit tatsächlich geht.

Die Sprache nach dem Fehlerfall wählen, den ich vermeide.

Rust ist dort, wo Korrektheit unter Nebenläufigkeit nicht verhandelbar ist. Der Borrow-Checker ist keine Steuer — er ist ein Beweis zur Kompilierzeit, dass eine Klasse von Data-Races und Use-after-free-Fehlern nicht auftreten kann, was genau das ist, was ich für einen performanzkritischen Kern will, der unbeaufsichtigt laufen muss.

Go ist dort, wo ich nebenläufige Netzwerkdienste will, über die ich schnell nachdenken kann: Goroutines und Channels für das Nebenläufigkeitsmodell, der eingebaute pprof-Profiler zum Finden des heißen Pfads und ein statisches Binary, das sich sauber in einen Container deployen lässt. C und C++ bleiben für die hardwarenahe Arbeit reserviert — Firmware, eingebettetes SoC, der Code, der direkt auf der Registerkarte sitzt.

  • Rust — Ownership, Lifetimes, kostenfreie Abstraktionen, furchtlose Nebenläufigkeit
  • Go — Goroutines, Channels, Context-Abbruch, pprof-Profiling
  • C / C++ — eingebettete Firmware, hardwarenahe Steuerung, deterministisches Timing
  • Bash und SQL in Produktion, über PostgreSQL und BigQuery
Multi-Cloud-Topologie

Eine Anwendung, drei Clouds, keine Bindung.

Ein Workload, den ich portabel halte: Container und Infrastruktur als Code im Zentrum, deploybar zu dem Anbieter, in dem ein Engagement bereits lebt. Die Cloud ist eine Warenschicht, keine Abhängigkeit.

App-Kern Docker · K8s · IaC GCP Cloud Run · GKE · Vertex AI Azure AKS · Functions · Apps AWS Lambda · EKS · S3 · RDS Edge CF Workers · Vercel Edge
01

GCP

Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore — meine Standardwahl für KI-Workloads und verwaltete Container-Auslieferung.

02

Azure

AKS, Functions, Cognitive Services, Container Apps — eingesetzt dort, wo eine Organisation bereits im Microsoft-Umfeld lebt.

03

AWS

Lambda, ECS/EKS, S3, RDS, SQS/SNS hinter CloudFront — die Breite an Primitiven für klassische Produktionsstacks.

04

Edge

Cloudflare Workers und Vercel Edge — Logik, die nah am Nutzer läuft, für Latenz und globale Reichweite.

05

BaaS

Supabase und Firebase (Auth, Firestore, Realtime DB) — wenn ein Produkt ein Backend in Tagen braucht, nicht in Wochen.

06

Auslieferung

Docker und Kubernetes, GitHub Actions und GitLab CI, Infrastruktur als Code — reproduzierbare Umgebungen von Anfang bis Ende.

Die Cloud sollte eine Ware sein, die ich austauschen kann, nie eine Abhängigkeit, die das Produkt besitzt.

Der Instinkt eines Werkzeugbauers
Reibung zweimal getan kleines Werkzeug ~/.local/bin die Toolchain summiert sich

Eine kontinuierliche persönliche Linux-Toolchain

Jede Reibung im Arbeitsablauf wird zu einem kleinen Programm.

Ich betreibe Linux auf Administratorebene — Kernel-Tuning, sysctl-Härtung, systemd-Units, Container-Runtimes und das Netzwerk darunter. Doch der Teil, der definiert, wie ich arbeite, ist kleiner: Über die Jahre habe ich Dutzende eigener Kommandozeilen-Werkzeuge geschrieben, eines nach dem anderen, jedes geboren aus einer Reibung, auf die ich mehr als einmal gestoßen bin.

Eine zweimal getane Aufgabe ist ein Kandidat für ein Werkzeug. Das Ergebnis ist eine Workstation, die zu meinen Händen passt statt zu den Standardeinstellungen — und, wichtiger, der Instinkt dessen, der das Werkzeug baut, statt die Lücke zu dulden. Dieser Instinkt ist derselbe, der am Ende in den Produkten ausgeliefert wird.

  • Kernel-Tuning, sysctl-Härtung, systemd, Container-Runtimes, Netzwerk
  • Dutzende selbstgeschriebener CLI-Werkzeuge, jedes löst eine wiederkehrende Reibung
  • Der Reflex eines Werkzeugbauers, nicht nur eines Werkzeugnutzers
Der Stack, auf einen Blick

Was ich in eine Konstruktion einbringe.

Engineering-Stack

Primäre Systemsprachen
Rust, Go, C/C++
Hochsprachen
Python, TypeScript/JS
Mobil — nativ
Swift, Obj-C, Kotlin, Java
Mobil — plattformübergreifend
React Native, Flutter
Backends
Node, Spring, Django, Rails, Go
API-Stile
REST, GraphQL
Datenspeicher
PostgreSQL, BigQuery, SQLite, Redis
Container
Docker, Kubernetes
CI/CD
GitHub Actions, GitLab CI
Linux
Administratorebene

Nichts davon ist eine Wunschliste. Jede Zeile ist ein Werkzeug, mit dem ich ausgeliefert habe — gewählt, in einem bestimmten Projekt, weil es die richtige Antwort für diese Schicht und diesen Fehlerfall war.

Der rote Faden durch alles ist dasselbe Temperament: das System vor der Datei bauen, für die Nähte entwerfen, die Auslieferung reproduzierbar halten und die Abhängigkeiten ablehnen, die das Produkt später besitzen würden.

Ereignisgesteuerte Architektur

Entkoppelt per Nachricht, nicht per Hoffnung.

Producer App · Gerät Producer Dienst Broker Pub/Sub · Queue Consumer Consumer Consumer Dead-Letter

Eine Producer- / Broker- / Consumer-Topologie

Komponenten sprechen über einen Broker, damit ein Ausfall das System verlangsamt statt es zu stoppen.

Wenn Dienste sich direkt aufrufen, wird eine einzige langsame Abhängigkeit zu einer Kaskade. Stattdessen entwerfe ich rund um einen Ereignis-Broker: Producer veröffentlichen Fakten über das Geschehene, der Broker hält sie dauerhaft, und Consumer verarbeiten im eigenen Tempo. Der Producer blockiert nie auf dem Consumer, und ein Consumer, der zurückfällt, leert seinen Rückstau, ohne Arbeit zu verlieren.

Die Ereignisse werden zur Quelle der Wahrheit über das Geschehene — eine Dead-Letter-Queue fängt, was nicht verarbeitet werden kann, Wiederholungen sind begrenzt und idempotent, und ein neuer Consumer kann denselben Stream abonnieren, ohne dass jemand den Producer ändert. Das ist die Eigenschaft, die ich kaufe: die unabhängige Weiterentwicklung von Teilen, die nie voneinander wissen müssen.

  • Producer veröffentlichen, Consumer abonnieren — keine direkte Kopplung
  • Ein dauerhafter Broker fängt Stöße ab; Consumer verarbeiten im eigenen Tempo
  • Dead-Letter-Queue und begrenzte, idempotente Wiederholungen bei Fehlern
Auslieferungspipeline

Von einem Commit zur Produktion, mit dem Artefakt, das nach vorn befördert wird.

Eine CI/CD-Pipeline, die ich als nicht verhandelbare Infrastruktur behandle. Das Image wird einmal gebaut und unverändert durch jedes Tor befördert, sodass das, was in Produktion läuft, byteweise das ist, was die Tests bestanden hat.

Von Commit zu Canary

  1. 01 Commit Push auf einen Branch. Pre-Commit-Hooks führen Formatierung und Lint lokal aus, sodass das Offensichtliche gefangen wird, bevor die CI überhaupt anläuft.
  2. 02 Build und Test Container-Image einmal gebaut, Unit- und Integrationstests dagegen ausgeführt, Coverage-Schwelle erzwungen. Dasselbe Artefakt wird nach vorn befördert.
  3. 03 Scan Statische Analyse, Abhängigkeits-Audit und Image-Schwachstellenscan. Ein fehlschlagender Scan blockiert den Merge, nicht das Gedächtnis eines menschlichen Prüfers.
  4. 04 Staging Das signierte Image in eine Staging-Umgebung deployen, die die Produktion spiegelt, Smoke-Tests und einen kurzen Soak ausführen.
  5. 05 Befördern In Produktion hinter einem Canary oder einem Blue-Green-Switch ausrollen, die Metriken beobachten und die vorherige Version einen Befehl entfernt halten.
Rollback — vorherige Version einen Befehl entfernt Commit Build+ Test Scan Staging CanaryProd-Rollout
Echtzeitdienste

Zustand über einen Socket getragen, mit den Fehlerfällen behandelt.

Client · ws Client · ws Client · ws Gatewayterminieren · leiten Handler Handler geteilter Zustandsspeicher

Ein Socket-Service-Mesh

Ein WebSocket-Gateway vorn, zustandsbehaftete Verbindungen dahinter, ein geteilter Store für Dauerhaftigkeit.

Echtzeitarbeit lebt oder stirbt an den unglamourösen Teilen: was passiert, wenn die Verbindung eines Clients mitten in einer Nachricht abbricht, wenn ein Consumer hinter den Producer zurückfällt, wenn dasselbe Ereignis zweimal ankommt. Ich terminiere WebSocket- und Socket-Verbindungen an einem Gateway, leite jede an einen zustandsbehafteten Handler und halte dauerhaften Zustand in einem geteilten Store, sodass ein wiederverbindender Client fortsetzen statt neu starten kann.

Backpressure ist explizit — begrenzte Buffer mit einer definierten Verwerfen-oder-Zusammenfassen-Richtlinie, nie eine unbegrenzte Queue, die still den Speicher frisst, bis der Prozess stirbt. Präsenz wird mit Heartbeats und TTL-Schlüsseln verfolgt, und die Auslieferung ist idempotent, damit ein At-least-once-Transport nicht zu At-least-twice-Verhalten für den Nutzer wird.

  • WebSocket-/Socket-Gateway terminiert und leitet Verbindungen
  • Zustandsbehaftete Handler pro Verbindung; geteilter Store für Dauerhaftigkeit
  • Heartbeats, TTL-Präsenz, Resume bei Wiederverbindung, idempotente Auslieferung

Echtzeit-Primitive

Transport
WebSocket, rohe TCP-Sockets, gRPC-Streams
Fan-out
Pub/Sub, Redis-Kanäle, SSE für eine Richtung
Backpressure
Begrenzte Buffer, Verwerfen-/Zusammenfassen-Richtlinien
Auslieferung
Idempotenzschlüssel, At-least-once + Dedupe
Präsenz
Heartbeats, TTL-Schlüssel, Wiederverbinden mit Resume
Zustand
Akteur pro Verbindung, geteilter Store für Dauerhaftigkeit
Ein zweites Stück des Echten

Zwei weitere Ausschnitte aus dem Stack im Einsatz.

Rusts Ownership und Gos Channels lösen dasselbe Problem — geteilter Zustand unter Nebenläufigkeit — von entgegengesetzten Enden: das eine bewegt das Ownership, sodass es nichts zu teilen gibt, das andere teilt durch Kommunizieren.

// Ownership moves; the compiler proves no use-after-move.
fn consume(buf: Vec<u8>) -> usize {
    buf.len() // buf is owned here, freed at the end
}

fn main() {
    let data = vec![1u8, 2, 3];
    let n = consume(data);
    // using 'data' here would not compile: it was moved.
    println!('consumed {} bytes', n);

    // Borrow instead of move when the caller keeps the value.
    let kept = vec![4u8, 5];
    let total: u8 = kept.iter().sum();
    println!('{} still owns {} items', total, kept.len());
}
// Share by communicating: a generator feeds a stage over a channel.
func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}
Edge-Handler

Logik, die nah am Nutzer läuft.

Ein TypeScript-Edge-Handler — von der Art, die ich auf Cloudflare Workers oder Vercel Edge deploye. Er läuft am Präsenzpunkt, der der Anfrage am nächsten ist, sodass eine Cache-Prüfung oder ein Auth-Gate nie eine Rundreise zu einer zentralen Region zahlt.

// Runs at the edge: serve from cache, fall through to origin once.
export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    const res = await fetch(req);
    if (res.ok && req.method === 'GET') {
      const cached = new Response(res.body, res);
      cached.headers.set('cache-control', 'public, max-age=60');
      ctx.waitUntil(cache.put(req, cached.clone()));
      return cached;
    }
    return res;
  },
};
Die Datenspeicher-Schicht

Der richtige Speicher für das Zugriffsmuster.

Ich wähle einen Datenspeicher nicht aus Gewohnheit. Ich wähle ihn nach der Form der Lese- und Schreibvorgänge, die er absorbieren muss, und akzeptiere, dass die meisten realen Systeme mehr als einen nutzen.

Ein relationaler Speicher verdient die Standardposition, weil die meisten Daten relational sind und Transaktionen ihren Preis wert sind. Aber der heiße Pfad, den ein Cache bedient, die analytische Frage, die Milliarden Zeilen scannt, und die Local-First-Persistenz auf einem Telefon sind drei verschiedene Probleme — und vorzugeben, dass eine Engine alle drei beantwortet, ist die Art, wie Systeme auf schwer rückgängig zu machende Weise langsam werden.

Die Disziplin unter ihnen allen ist dieselbe: Das Schema lebt als versionierte, umkehrbare Migrationen im Repo, Indizes werden gegen den tatsächlichen Abfrageplan entworfen statt geraten, und die Abfragen auf dem heißen Pfad werden gelesen und von Hand abgestimmt, auch wenn ein ORM den ersten Entwurf geschrieben hat.

01

PostgreSQL

Der relationale Standardspeicher — Transaktionen, JSONB wo es seinen Platz verdient, und Indizes, die gegen den Abfrageplan entworfen statt geraten werden.

02

BigQuery

Analytische Skalierung. Spaltenspeicher und Partitionierung für die Fragen, die Milliarden Zeilen scannen, aber selten laufen.

03

SQLite

Persistenz auf dem Gerät und eingebettet für Mobil und Edge — eine einzige Datei, kein Server, das richtige Werkzeug für Local-First-Apps.

04

Redis

Der heiße Pfad — Caching, Rate-Limiting, Warteschlangen, flüchtige Präsenz und Locks, wo Mikrosekunden und TTLs zählen.

05

ORMs

Sequelize, Mongoose, GORM, wo sie die Auslieferung beschleunigen — aber das generierte SQL bleibt lesbar, und die heißen Abfragen werden von Hand geschrieben.

06

Migrationen

Schema als versionierte, umkehrbare Migrationen im Repo, sodass eine Datenbank reproduzierbar statt archäologisch ist.

Linux-Härtungs-Stack

Der Kernel, auf dem der Container läuft, als Teil des Produkts behandelt.

Updates Unbeaufsichtigte Sicherheits-Patches, reproduzierbare Images Audit auditd, journald-Aufbewahrung, Log-Versand aus dem Host Prozess systemd-Sandboxing, seccomp, Namespaces, cgroups Netzwerk nftables Default-Deny, fail2ban, segmentierte Ports Identität Nutzer mit minimalen Rechten, sudo-Richtlinie, nur SSH-Schlüssel Boot und Kernel Signierter Boot, minimale Module, sysctl-Baseline außen — exponierte Fläche, überwacht und protokolliert

Ein Defense-in-Depth-Stack

Sicherheit als Schichten, vom signierten Boot bis zum Audit außerhalb des Hosts, nicht eine einzige Firewall-Regel.

Ich administriere Linux bis zum Kernel und behandle den Host als Teil der Angriffsfläche statt als trägen Ort, an dem ein Container läuft. Der Stack liest sich von unten nach oben: ein minimaler signierter Boot und eine sysctl-Baseline, Nutzer mit minimalen Rechten mit SSH-Schlüsseln und ohne Passwörter, eine Default-Deny-Firewall, Prozessisolation durch systemd-Sandboxing und cgroups und Audit-Logs, die aus dem Host versandt werden, sodass eine Kompromittierung ihre eigene Spur nicht still löschen kann.

Nichts davon ist exotisch — es ist die gewöhnliche Disziplin, einen Host so zu betreiben, als würde er angegriffen, weil er es irgendwann wird. Derselbe Instinkt, der eine kleine CLI für eine wiederkehrende Reibung baut, baut einmal ein gehärtetes Basis-Image und verwendet es überall wieder, sodass der sichere Weg auch der einfache Weg ist.

  • Signierter Boot, minimale Kernel-Module, sysctl-Baseline
  • nftables Default-Deny, systemd-Sandboxing, seccomp, cgroups
  • auditd und Log-Versand außerhalb des Hosts; unbeaufsichtigte Sicherheitsupdates
Der Bogen

Wie sich der Stack ansammelte, Schicht für Schicht.

Kein einzelnes Jahr lehrte all dies. Es sammelte sich an, wie ein Stack es sollte — vom Glas, das der Nutzer berührt, abwärts, bis der Kernel so vertraut war wie die Aussicht.

  1. Jahre 1–3 Native Mobil-Apps, von Anfang bis Ende Swift und Objective-C auf iOS, Kotlin und Java auf Android. Ich lernte die Disziplin, an Geräte im Feld auszuliefern — Fragmentierung, Offline, Akku- und Speicherbudgets.
  2. Jahre 3–6 Die Backends hinter den Apps Node, Spring, Django und Rails, die REST und GraphQL bedienen, gestützt von PostgreSQL und Redis. Die App hörte auf, an der API zu enden, und begann am Kernel.
  3. Jahre 6–9 Plattformübergreifend und Cloud React Native und Flutter, wo sich eine geteilte Codebasis auszahlte; Container, Kubernetes und Multi-Cloud-Auslieferung, sodass die Infrastruktur reproduzierbar wurde.
  4. Jahre 9–heute Tiefe in Systemen und Verteiltem Rust und Go für Kerne, die korrekt und schnell sein müssen, ereignisgesteuerte und Echtzeit-Topologien und eine kontinuierliche persönliche Linux-Toolchain unter alldem.

Open to the right work

Wenn Sie einen Ingenieur brauchen, der den gesamten Stack halten kann — vom Kernel bis zur Cloud — ist das die Arbeit, die ich mache.

If you are holding a problem that doesn't fit inside one field, that is the conversation I want.

NextDefense Telecom