Ein Lohnzettel ist zweifellos eines der am häufigsten gelesenen und am wenigsten verstandenen Dokumente im Leben eines Angestellten. Man erhält ihn jeden Monat, bewahrt ihn bis zur Rente auf, und doch ist kaum jemand in der Lage zu erklären, wie man vom Bruttolohn zur tatsächlich ausgezahlten Summe kommt. Diese Undurchsichtigkeit ist kein Zufall: Hinter jeder Zeile verbirgt sich eine Anhäufung von Regeln, Ausnahmen und Sonderfällen, die sich über Jahrzehnte angesammelt haben.
Für einen Ingenieur verbirgt diese Beobachtung eine andere, interessantere. Ein Lohnzettel ist nichts anderes als die deterministische Ausführung eines Regelbaums : eine Funktion, die als Eingabe etwa hundert Variablen (Gehalt, Status, Dienstalter, Krankenkasse, Abwesenheiten, Kumulierungen seit Januar…) entgegennimmt und eine Reihe von Beträgen erzeugt. Das eigentliche Thema ist also nicht die Berechnung, er ist perfekt deterministisch, aber die Art und Weise, wie Tausende von miteinander verbundenen Regeln kodiert werden, ohne dass die Codebasis ihrerseits zu schwarzer Magie wird.
Das ist die Wette, die Puncto eingegangen ist, ein
Lohnsoftware französisches SaaS: Anstatt das Arbeitsgesetzbuch in imperatives TypeScript zu übersetzen, die Vorschriften in einer deklarativen DSL zu beschreiben,
Publicodes, und dann von einem typisierten Back-End aus auszuführen. Hier erfahren Sie, wie dies geschieht und welche technischen Probleme es wirklich aufwirft.
Das Arbeitsgesetzbuch ist eine Legacy-Codebasis
Die Lohnvorschriften haben genau die Eigenschaften, die man bei einem alten Monolithen fürchtet:
Gestapelte Regeln : jede Abgabe (Gesundheit, Rente, Arbeitslosigkeit, CSG-CRDS, AT/MP…) hat ihren Satz, ihre Bemessungsgrundlage, ihre Obergrenzen, ihre Abzüge.
Überall Ausnahmen : Lehrling, Führungskraft, Praktikant, Tagespauschale… die gleiche Abgabe wird je nach Status unterschiedlich berechnet.
Ein sich entwickelndes Sozialmodell : Sätze, SMIC und Obergrenze der Sozialversicherung ändern sich mindestens einmal pro Jahr, manchmal auch unter dem Jahr.
Zirkuläre Abhängigkeiten : bestimmte Werte definieren sich in Abhängigkeit von sich selbst.
Dies imperativ zu kodieren ist machbar, aber man erhält schnell Tausende von Zeilen verschachtelter Bedingungen: manuell zu verwaltende Auswertungsreihenfolge, implizite Abhängigkeiten und eine Basis unlesbar für die einzigen Personen, die sie validieren können : die Lohnexperten, die keine Entwickler sind. Das Problem ist nicht, die Berechnung zu schreiben, sondern sie zu behalten überprüfbar.
Publicodes: eine Vorschrift ausführbar machen
Publicodes ist eine deklarative Open-Source-Sprache, ursprünglich vom Team von
mon-entreprise.urssaf.fr entwickelt, um das französische Sozialrecht zu modellieren. Das Prinzip: Man beschreibt
Regeln und ihre Abhängigkeiten, nicht einen Algorithmus. Die Engine konstruiert den Graphen und bewertet ihn selbst, wie eine Tabellenkalkulation ihre Zellen neu berechnet.
Eine «Blatt»-Regel reduziert sich auf eine Frage:
salaire de base:
question: Salaire de base ce mois ?
unité: €
Eine berechnete Regel setzt sich aus anderen Regeln durch deren Namen zusammen:
rémunération brute:
unité: €
somme:
- salaire de base
- total primes
- indemnisations d'absences
Daraus ergeben sich direkt drei Punkte. Erstens, der Name der Regel ist ihre ID : man referenziert Grundlohn durch seine französische Bezeichnung, das Modell ist so wie es ist lesbar. Zweitens, man kodiert niemals die Berechnungsreihenfolge : die Engine leitet daraus den Abhängigkeitsgraphen ab und bewertet ihn bei Bedarf. Schliesslich ist diese Datei ist die Spezifikation: ein Lohnexperte liest sie durch und validiert sie Zeile für Zeile, was mit dem imperativen Äquivalent unmöglich wäre. Bei Puncto repräsentiert dieses Modell heute einige hundert Regeln, verteilt auf mehrere Dateien und in grosse Module gegliedert.
Die wahren Herausforderungen
Ein «Hello World» Publicodes ist trügerisch einfach. Die Schwierigkeit entsteht, wenn sich die Regeln verflechten. Einige Beispiele, die aus der echten Codebasis stammen, veranschaulichen dies.
1. Zirkuläre Abhängigkeiten
Im Krankheitsfall zahlt der Arbeitgeber oft den Lohn weiter: er ergänzt die von der Sozialversicherung gezahlten Tagesgelder (IJSS). Diese IJSS müssen abgezogen werden vom Brutto abgezogen werden, um eine Doppelzählung zu vermeiden… aber das Brutto dient gerade dazu, einen Teil dessen zu berechnen, was davon abhängt. Zirkuläre Referenz. Im Imperativen würde man sie mit einem zweistufigen Berechnungslauf behandeln. Publicodes legt sie als deklarative Eigenschaft dar:
rémunération brute:
résoudre la référence circulaire: oui
unité: €
somme:
- salaire de base
- total primes
- indemnisations d'absences
- IJSS brutes à soustraire
avec:
IJSS brutes à soustraire:
applicable si: maintien du salaire = oui
valeur: total IJSS ce mois * -1
Mit die zirkuläre Referenz auflösen, die Engine erkennt den Zyklus und iteriert, bis sich der Wert stabilisiert (Fixpunkt). Die Komplexität ist deklariert, nicht in einer hausgemachten Orchestrierung gebastelt, die man bei jeder Änderung neu testen müsste.
2. Die numerische Inversion: das Brutto vom Netto aus ermitteln
In Frankreich wird ein Gehalt fast immer als Brutto angegeben. Es kommt jedoch vor, dass ein genaues Netto garantiert werden muss: ein bei der Einstellung ausgehandeltes garantiertes Netto oder eine Netto-Regulierung am Jahresende. Die gesamte Lohnbuchhaltungsmaschinerie läuft jedoch in die andere Richtung: man geht vom Brutto aus, zieht die Abzüge und dann die Steuern ab, und erhält das Netto. Wie kann man das Brutto ermitteln, das genau ein gegebenes Netto ergibt?
Der Reflex wäre, die inverse Formel zu schreiben. Nur gibt es keine: das Netto in Abhängigkeit vom Brutto ist eine nicht-lineare Treppenfunktion (Tranchen, Obergrenzen, Freibeträge). Man kann sie nicht algebraisch invertieren. Der einzige zuverlässige Weg ist numerisch: ein Brutto ausprobieren, das entsprechende Netto berechnen, mit dem Ziel vergleichen, anpassen, wiederholen bis zur Konvergenz. Auch hier stellt Publicodes den Mechanismus bereit, dienumerische Inversion : man deklariert, dass eine üblicherweise «vorwärts» berechnete Regel aus einer ihrer Ausgaben ermittelt werden kann.
rémunération brute calculée depuis le net:
unité: €
applicable si: option de régularisation du net
inversion numérique:
- net à payer avant impôt sur le revenu
remplace:
références à:
rémunération brute
Die Engine lässt dann die gesamte Brutto → Netto-Berechnung in einer Schleife laufen, indem sie das Brutto bei jeder Iteration anpasst, bis das berechnete Netto dem Ziel entspricht (eine Nullstellensuche). Zwei elegante Details: ersetzt führt dazu, dass dieses «invertierte» Brutto automatisch das Brutto überall im Modell ersetzt, sobald ein Netto bereitgestellt wird, und anwendbar wenn hält den Mechanismus den Rest der Zeit inaktiv. Vor allem schreibt man kein zweites Modell «umgekehrt» neu: es ist derselbe Regelbaum, der in die andere Richtung ausgeführt wird. Verwandt mit der oben genannten zirkulären Referenz, drückt die numerische Inversion gut aus, was diese Engine ist: ein iterativer Solver, keine einfache Formelkaskade.
Die Brücke zwischen Publicodes und TypeScript
Hier spielt sich der interessanteste Teil aus Ingenieursicht ab: ein in Französisch geschriebenes Modell mit einem typisierten Back-End in Dialog treten zu lassen, ohne die Garantien des einen oder anderen zu verlieren.
Bei der Kompilierung, die Dateien .publicodes werden in einen JSON-Graph umgewandelt und in generierte TypeScript-Typen: Die Vereinigung aller Regelnamen existiert nun als Typ. Konkret wird der Aufruf einer Regel autovervollständigt, und das Anfordern einer nicht existierenden Regel wird zu einer Kompilierungsfehler, nicht zu einer Laufzeitausnahme mitten in der Erstellung eines Belegs. Pro Beleg wird eine Engine instanziiert, dann wird bei Bedarf ausgewertet:
const { payslipCalculator } = getPayslipCalculator(payslipVariables)
const net = payslipCalculator('net payé')
const superBrut = payslipCalculator('super brut')
Die Impedanz bleibt: Publicodes hat seine Konventionen, die vom Recht stammen (ein Boolescher Wert wird geschrieben ja/nein, ein Datum JJ/MM/AAAA, eine Enumeration wird zitiert: 'CDI'). Ein typisierter Wrapper überbrückt in beide Richtungen, und vor allem weigert er sich, ein Ergebnis zu liefern, wenn eine erforderliche Variable fehlt:
calculateValue<T extends RuleNames>(value: T) {
const res = super.evaluate(value)
const missingVariables = Object.keys(res.missingVariables)
.filter((key) => key.includes('régularisation') === false)
if (missingVariables.length > 0) {
throw new Error(`Missing publicodes variables ${JSON.stringify(missingVariables)}`)
}
return res.nodeValue as RuleValue[T]
}
Diese Wahl des fail-fast ist eine eigenständige Designentscheidung. In einem Bereich, in dem eine vergessene Variable keinen Absturz verursacht, sondern einen Betrag produziert, falsch, ist eine explizite Ausnahme besser als ein stillschweigend fehlerhafter Beleg.
Eine Berechnung testen, bei der ein Fehler teuer ist
Sobald dieses Fundament gelegt ist, findet ein mentaler Wandel statt, und dieser ist kontraintuitiv: die Berechnung macht praktisch nie Fehler. Ein validierter Regelbaum liefert immer das richtige Ergebnis für korrekte Eingaben. In der Praxis kommen Fehler bei der Lohnabrechnung fast ausschliesslich von einer Parametrierung oder einer Erfassung : falsch zugeordnete Krankenkasse, falsches Dienstalter, vergessene Prämie, falsch gemeldete Abwesenheit.
Die Teststrategie folgt dieser Logik: man friert komplette Situationen ein und prüft jede Zeile bis auf den Rappen genau. Ein Manager-Beleg von 64 k€/Jahr vom April 2025 erzeugt so rund hundert Zusicherungen des Typs:
test('CSG déductible', () => {
assert.strictEqual(
payslipCalculator("CSG-CRDS . CSG déductible de l'impôt sur le revenu"),
360.7,
)
})
Diese Tests spielen eine doppelte Rolle: Antiregressionsnetz, und ausführbare Spezifikation. Wenn ein Satz am 1. Januar wechselt, fügt man eine datierte Situation mit den neuen erwarteten Beträgen hinzu; die Nicht-Regression bei älteren Jahrgängen ist konstruktionsbedingt durch eine datumsbasierte Versionierung auf Publicodes-Ebene garantiert. Ergebnis: das Ändern einer Beitragsregel, das normalerweise in einer Lohnsoftware gefürchtet ist, wird zu einem beherrschbaren Vorgang: wenn sich irgendwo ein Rappen bewegt, signalisiert ein Test dies sofort.
Was die Deklaration bringt und was sie kostet
Die Bilanz ist klar. Auf der Habenseite von Publicodes: ein Modell von den Fachexperten auditierbar selbst, lesbare Git-Deltas wenn sich eine Regel ändert, eine automatische Abhängigkeitsauflösung (keine manuelle topologische Sortierung), eine regulatorische Versionierung nahezu kostenlos über Datums-Variationen - alles verpackt in einer Typenschicht, die die statische Sicherheit von TypeScript wiederherstellt.
Es gibt Kosten: die Konventionen der Sprache lernen, die Impedanzschicht schreiben und warten, und akzeptieren, dass eine Engine, die Zyklen durch Iteration löst, etwas weniger transparent ist als eine von Hand abgewickelte Funktion. Bezogen auf ein lebendiges, rückwirkendes und mit Ausnahmen gespicktes Sozialrecht ist der Austausch sehr vorteilhaft.
Die Lektion geht über die Lohnabrechnung hinaus: wenn ein Bereich einer Legacy-Codebasis ähnelt, die niemand vollständig beherrscht, ist die richtige Antwort nicht immer, mehr Code zu schreiben: manchmal ist es, die richtige Sprache zu finden, um es zu beschreiben, und eine Maschine es ausführen zu lassen. Genau das treibt die Lohnabrechnungs-Engine von
Puncto im Alltag an.