ADR-0005: The semantic model is the backend-interface currency; storage formats are swappable adapters
- Status: Accepted
- Date: 2026-06-08
- Related: ADR-0002 (backend abstraction), ADR-0004 (MPR backend uses the codec internally);
docs/11-proposals/ASSESSMENT_harvest_engalar_writes.md
mxcli turns Mendix-model operations into changes against a project. Three layers can carry an operation:
- AST — the MDL parse tree. Transient, syntax-shaped, MDL-only, write-only (BSON is never parsed into an AST).
- Semantic model (
sdk/microflows,sdk/domainmodel,model) — engine-agnostic Mendix concepts: clean names, resolved references, normalized. - gen + codec (
modelsdk/gen,modelsdk/codec) — a 1:1 mirror of the current MPR BSON storage shape, with its quirks (storage names, theConcurrenyErrorMessagetypo, binary refs, list markers) and a roundtrip-safe codec.
The engalar fork, having exactly one front-end (MDL) and one back-end (MPR/BSON), builds AST→gen directly and exposes a gen-typed backend interface (ListMicroflowsGen); for that scope a semantic model is pure overhead. mxcli's scope is different:
- Multiple front-ends today: the MDL executor and the fluent
api/builders (plausibly future importers / an LLM tool surface). - Multiple back-ends today: MPR (BSON via codec) and MCP/PED (live Studio Pro, a JSON-operation API that is not BSON).
- A third back-end is anticipated: Mendix is considering replacing the BSON storage format with one that is more diff-friendly, separates interface from implementation storage (lazy/granular loading), is smaller, and enables runtime hot-reload.
The question: should operations flow through a semantic model, or go AST→gen directly like engalar?
The backend interface speaks the semantic model, not storage (gen) or syntax (AST) types. Concretely:
- Front-ends produce the model (MDL executor, fluent
api/, future importers). - Reads return the model; the model is the currency for SHOW/DESCRIBE/catalog/lint/diff so consumers are backend-agnostic and storage-stable.
- gen + codec are an implementation detail of the MPR backend — one storage adapter among several (MPR, PED, future), never the contract.
- CREATE goes model→storage (the MPR backend does model→gen→BSON). ALTER uses backend-internal mutation (gen-mutation / the
OpenXForMutationmutator pattern for MPR) rather than a model round-trip, so the model need not be a perfectly lossless mirror and edits preserve fidelity. - The model distinguishes interface (signature) vs implementation (body) granularity, and backends may load them lazily (
List…≈ signatures,Get…≈ body) — matching both current catalog/lint performance needs and the anticipated interface/implementation-split storage format.
Positive.
- A new storage format (the one Mendix is planning) becomes just another backend (
model→newformat) — the executor, front-ends,api/, lint, catalog, and diff are untouched. The model is the anti-corruption layer against exactly the storage churn that is coming. - MCP/PED and
api/remain first-class without contorting them into BSON/gen shapes. - The codec's byte-passthrough roundtrip-safety — a coping mechanism for today's BSON churn — is correctly scoped to one adapter, so it can be retired with BSON rather than being load-bearing.
Negative.
model→storageper backend is more code than AST→storage for the single-backend create path (the "duplication" felt while hand-portingmicroflowToGen). Accepted: with N backends this is the DRY factoring (parse once → AST→model; serialize per backend), not waste.- The model must be maintained alongside gen. Mitigation: keep it semantic, not a gen mirror, and route fidelity-sensitive ALTER through gen-mutation rather than forcing a lossless model round-trip.
Neutral.
- This sits above ADR-0004: ADR-0004 governs the MPR backend's internal choice (use the codec for all document types, not a codec/legacy hybrid); ADR-0005 governs the interface (model-typed, gen is internal). They compose.
engalar's gen-typed, AST-direct architecture. Build AST→gen, expose a gen-typed interface, drop the model. Rejected for mxcli: it welds the backend contract to the BSON shape, which (a) makes the MCP/PED backend unnatural (it would have to emit/consume gen trees for a non-BSON API), and (b) would require reworking the contract when Mendix replaces BSON. It is the right call for engalar's single-backend scope and the wrong one for ours. We still harvest engalar's flowBuilderGen as a reference for how to construct each gen element inside the MPR backend's model→gen — but not its interface or executor architecture.
Build the model around the future format now. Rejected as premature (YAGNI): the future format is unspecified. The justification for the model seam is the three concrete consumers we have today (MPR, MCP, api/); the coming format only confirms the hedge is aimed at the right risk. We build the seam, not the format.