Adding a domain without a fork

Austin Wagner

I built a risk-prediction service for loan covenants, then generalised it so a new subject is a registration rather than a fork. Two more domains have since been added without touching the shared training or explanation libraries; what they did change was the seam they plug into, and that is the honest shape of generalising something that exists.

Problem
A model service written for one subject usually has that subject's assumptions baked through it, so the second subject means a copy.
Result
Weather and esports added behind one protocol; 0 files changed in the two shared libraries by either addition. Covenant, the original, is still served directly.
Models
12 backends behind one interface: 7 classifiers and 5 regressors, swappable per request.
Data
40 real public datasets, not synthetic ones.
Stack
Python, PyTorch, XGBoost, LightGBM, Optuna, Kafka on Confluent Cloud.

The claim, stated exactly

Pluggability is easy to assert and the assertion is usually worth nothing, so here is the version git can check. Weather and esports each implement one protocol and register. Covenant, being first, is served by the service directly and is not itself a plug-in.

Domain addedShared libraries touchedPlug-in seam touched
weather0 filesDomainProtocol reshaped (37 lines added, 62 removed) and the generic worker's call into it (4 added, 6 removed)
esports0 filesthe registry moved from instances to factories (40 added, 17 removed) and the entry point with it (38 added, 17 removed)

So the training and explanation code never learned that weather or esports exist. The protocol did: the second domain is where decode-and-extract became one call the domain owns, and the third is where construction was deferred so a deployment running only esports never loads weather's fitted state. That is what the second and third implementations are for.

class DomainProtocol(Protocol):
    """Protocol that all domain implementations must satisfy.

    A domain provides event codecs, feature extraction, alert context
    generation, and Kafka topic configuration. The GenericStreamingWorker
    delegates all domain-specific logic through this protocol.
    """

services/covenant-radar-api/src/covenant_radar_api/domains/protocols.py, the protocol both plug-ins implement.

Rules and models kept deliberately apart

Covenant compliance is exact rule evaluation: formulas, thresholds, frequencies. Breach risk is the learned path. They are separate code paths on purpose.

Conflating them would make a compliance answer probabilistic, which is worse than useless. A lender asking whether a borrower is in breach needs an exact answer from the contract terms, not a confidence score. The temptation is real because one model could nominally produce both.

Substitutable underneath, stable on top

The same idea runs through the model layer and the explanation layer. Twelve backends sit behind one interface, including gradient boosting I wrote from scratch competing against the production libraries in the same harness, and they are selectable per request.

Explainability follows the model rather than the endpoint: permutation importance for anything, SHAP tree explanations for tree models, plain and integrated gradients for the neural ones. One contract, three strategies, chosen by what is actually loaded.

Training on 40 real public credit, loan, default and bankruptcy datasets rather than generated data is the unglamorous half. Synthetic data is the usual shortcut, and it flatters everything.

Record

Every figure above is bound to a named section of the wiki page covenant-radar and checked mechanically before this page can deploy. That page records how each count is reproduced and three ways of getting it wrong, including one met while re-checking for this article.

Known limits. No accuracy figures are claimed here: this is a statement about architecture, not about how well the models score. The headline has been corrected twice. The source page first said all three domains were plug-ins, and two are. Then, until 2026-09-14, this page said the two additions touched the shared machinery "not at all"; the commits that added them show the protocol, worker and registry edited, and only the libraries untouched, so it now says that.

Sources