Auf dieser Seite

Fachregeln als Daten: eine Rule Engine statt if-Kaskaden

„Produkt X nur anzeigen, wenn die Lieferung nach Deutschland geht.” „Bei Bestellungen mit Kindern das Familienpaket nach oben sortieren.” Solche Regeln beginnen als if im Code und enden als Kaskade, die niemand mehr anfassen möchte. Jede Änderung braucht dann ein Ticket, ein Review und ein Release.

Ich habe für genau dieses Problem einen Service mit einer generischen Rule Engine gebaut. Die Regeln sind seitdem Daten. Dieser Artikel zeigt den Aufbau und die Stellen, an denen es knifflig wurde.

Eine Regel besteht aus Bedingungen und Aktionen

Eine Regel hat einen Namen, eine Priorität, einen Gültigkeitszeitraum, eine Liste von Bedingungen und eine Liste von Aktionen:

{
  "name": "Familienpaket bei Kindern zuerst",
  "resource": "OFFERS",
  "priority": 20,
  "validFrom": "2026-01-01T00:00:00Z",
  "queries": [
    { "context": "CART", "query": { "persons": { "$elemMatch": { "type": "CHILD" } } } },
    { "context": "OFFER", "query": { "category": "FAMILY" } }
  ],
  "actions": [
    { "type": "SORT", "mode": "CUSTOM", "field": "category", "values": ["FAMILY"] }
  ]
}

Jede Bedingung bezieht sich auf einen Kontext: den Warenkorb, das Angebot, die Kategorie. Die Engine kennt drei Aktionen. FILTER schließt Ziele ein oder aus, SORT ordnet sie alphabetisch, numerisch oder nach einer festen Werteliste, und OVERWRITE setzt ein Feld auf einen anderen Wert.

Die Regeln laufen nach Priorität durch eine Pipeline aus Filtern, Sortieren und Überschreiben. Das Ergebnis einer Regel ist die Eingabe der nächsten. So lassen sich kleine Regeln kombinieren, statt eine große zu schreiben.

Eine Abfragesprache, die alle schon kennen

Für die Bedingungen brauchte ich eine Sprache. Ich habe keine neue erfunden, sondern die Syntax und Operatoren von MongoDB-Abfragen übernommen: $eq, $ne, $gt, $lt, $in, $nin, $exists, $elemMatch, $size, $and, $or, $not, dazu Pfade mit Punkt wie items.0.price.

Das hat drei Vorteile:

Der letzte Punkt ist der eigentliche Trick. Jedes Objekt wird vor der Auswertung in eine Map<String, Object> umgewandelt:

Map<String, Object> target = objectMapper.convertValue(cart, MAP_TYPE);
boolean matches = queryEngine.matches(target, rule.query());

Die Engine weiß nichts über Warenkörbe oder Angebote. Kommt ein neuer Kontext hinzu, braucht die Engine keine Zeile neuen Code. Regel-Queries können ebenso maximal flexibel formuliert werden und bedeuten keinen zusätzlichen Entwicklungsaufwand.

Anfragen ohne Datenbank

Der Unterschied zu MongoDB liegt in der Herkunft der Daten: Die Daten, auf die die Regeln angewandt werden und auf denen die Ergebnisse basieren, liegen im Kundenprojekt nicht persistiert in einer Datenbank, sondern kommen zur Laufzeit als Anfragekontext in den Microservice. Nur die Regeln liegen in einer dokumentenorientierten Datenbank bzw. im verteilten Cache.

Mit der Anfrage verbundene Daten lassen sich anhand des Kontexts ebenso zur Laufzeit verknüpfen. So können die Regeln auf die Anfrage unmittelbar angewandt werden, um direkt im Anschluss eine personalisierte Antwort zu liefern.

Drei Stellen, an denen es knifflig wurde

Sonderzeichen in der Datenbank. Abfragen enthalten Schlüssel mit $ und Pfade mit .. Beides erlaubt eine dokumentenorientierte Datenbank nicht als Feldnamen. Die Lösung ist ein Konverter, der die Zeichen beim Speichern maskiert und beim Laden wiederherstellt. Das fällt erst auf, wenn die erste Regel gespeichert werden soll.

Logische Operatoren neben anderen Schlüsseln. Steht ein $or auf oberster Ebene neben weiteren Bedingungen, ist nicht für alle offensichtlich, wie beides zusammenwirkt. Ich schreibe deshalb in allen Regeln ein ausdrückliches $and außen herum. Eine Zeile mehr, kein Missverständnis.

Bedingung oder Auswahl? Eine Abfrage auf das Angebot selbst ist keine Bedingung für die Regel, sondern wählt aus, auf welche Angebote die Aktion wirkt. Diese Unterscheidung muss in der Engine sauber getrennt sein, sonst überschreibt eine Regel plötzlich alle Preise statt einen.

Regeln sind Daten, aber nicht ungeprüft

Dass eine Regel ohne Release live geht, heißt nicht, dass sie ungetestet live geht:

Wann sich das lohnt

Eine Rule Engine ist zusätzlicher Code, der gepflegt werden will. Sie lohnt sich, wenn sich Regeln häufiger ändern als der Code, wenn mehrere Abläufe dieselben Regeln brauchen und wenn Fachleute die Regeln nachvollziehen sollen. Für drei Regeln, die sich einmal im Jahr ändern, bleibt das if die bessere Wahl.

Das zugehörige Fallbeispiel beschreibt, was die Engine im Einsatz gebracht hat.