Ich hasse Spaghetti-Code. Unter anderem Warzone 2100 hat mir 2011 gezeigt warum. C-Code, Globals überall. Man ändert eine Variable und drei Dinge brechen, die scheinbar nichts damit zu tun haben. Man liest den Code und weiß nicht, wo eine Entscheidung anfängt und wo sie aufhört.

3 Jahre - von 2022 bis 2025 habe ich an go-orb gearbeitet. Allein. Ein Framework für verteilte Systeme, gedacht als Nachfolger von go-micro. Config von überall, service discovery, RPC, streaming, pub/sub, kvstore, metrics. Jede neue Idee wurde ein Feature, jedes Feature ein Plugin.

Die Frage war immer: was könnte ein Nutzer davon brauchen?

Irgendwann hat go-orb alles gekonnt. Die Plugin-Architektur von go-micro machte es einfach – und fügte doch immer mehr Code hinzu, der die Komplexität weiter aufgebläht hat.

@bketelsen hatte eine frühe Version von incus-compose, ich später meine eigene. Docker Compose für Incus — eine Idee die ich sofort verstand. Ich hab einen Patch geschickt, der nie ankam. Seine Version ist gestorben.

Trotzdem hat sie alles ermöglicht. Sie hat bewiesen dass das Konzept funktioniert und den Pfad geräumt. Ohne sie wäre mein Start viel schwerer gefallen.

Erste eigene Version. Ich wollte es schnell zum Laufen bringen. Alles landete in einem Client-Struct: EnsureService, EnsureNetwork, EnsureImage, AttachPoolVolume. Eine Methode pro Ressource-Typ, alle auf demselben Objekt. Die compose-Typen direkt drin.

Es lief. Ich wusste sofort: das wird mich einholen.

Es hat mich eingeholt. Also zurück zu den Prinzipien: KISS (Keep it simple, stupid). Der zweite Versuch setzte genau darauf: Resources und Prioritäten. Ich hab jeder Ressource eine Nummer gegeben: Profile 512, Images 1024, Netzwerke 2048, Instanzen 8192. Erst kommt was eine kleinere Zahl hat. Keine Dependency-Graphen, kein Topologischer-Sortierung-Overhead — nur Zahlen und sortierbar.

Die Idee war richtig. Aber die Teile haben sich noch zu viel gegenseitig gekannt. Verbindungen wo keine sein sollten.

Dritter Versuch. Ich hab mit einer anderen Frage angefangen.

Was braucht jemand wirklich, um Container mit incus-compose zu betreiben?

up, down, list.

Ein Resource-Interface das jede Ressource gleich behandelt. Einen Stack der nach Priorität ordnet und ausführt. Hooks vor und nach jeder Action — Logging, Dry-Run, Fehlerkontext, alles ohne den Core anzufassen. Das war alles.

Ein Hook sieht so aus:

client.AddHookBefore(func(action Action, r Resource, opts Options, err error) error {
    if dryRun {
        fmt.Printf("would %s %s\n", action, r.Name())
        return ErrAborted
    }
    return err
})

Der Core weiß nichts davon. Er ruft den Hook auf, der Rest ist Sache des Aufrufers.

Sentinel-Errors reichern sich selbst an:

return ErrNotFound.WithResource(r)
// → "not found: Instance(web-1)"

Kein fmt.Errorf("instance %s: %w", name, err) mehr. Jeder Fehler trägt seinen Kontext mit — automatisch, weil der After-Hook das für jede Ressource erledigt.

Als ich das erste Mal eine neue Ressource hinzugefügt hab und sie einfach in den Stack gepasst hat — ohne Sonderfälle, ohne Anpassungen an anderen Stellen — hab ich kurz aufgehört zu tippen.

Boring. Genau so soll das sein.

Es hat angefangen, Spaß zu machen.

go-orb: drei Jahre Arbeit. Keine Nutzer. incus-compose: zwei Monate. Mehrere.


Von René Jochum und Claude (Anthropic). Lizenz: CC-BY-4.0.