Design Decisions
Key architectural decisions made during the development of ModelSDK Go, with rationale and trade-offs.
Pure Go SQLite (No CGO)
Decision: Use modernc.org/sqlite instead of mattn/go-sqlite3.
Rationale: mattn/go-sqlite3 requires CGO and a C compiler, complicating cross-compilation and deployment. The pure Go implementation works on all platforms without external dependencies.
Trade-off: Slightly lower SQLite performance compared to the C implementation. In practice, MPR files are small enough that this is not measurable.
BSON with MongoDB Driver
Decision: Use go.mongodb.org/mongo-driver for BSON parsing.
Rationale: Mendix stores model elements as BSON documents. The MongoDB Go driver provides a mature, well-tested BSON implementation. No actual MongoDB connection is needed – only the BSON codec is used.
SQL-Like DSL (MDL)
Decision: Create a custom SQL-like language instead of using JSON, YAML, or a general-purpose language.
Rationale:
- SQL is familiar to most developers
- LLMs generate SQL-like syntax with high accuracy
- 5-10x more token-efficient than JSON representations
- Human-readable diffs are easier to review than binary or JSON changes
- High information density for scanning microflow logic
Trade-off: Requires building and maintaining a custom parser (ANTLR4).
ANTLR4 for Parsing
Decision: Use ANTLR4 instead of hand-written parser or Go-native parser generators.
Rationale:
- Grammar is shareable across languages (Go, TypeScript, Java, Python)
- Well-defined grammar file serves as the language specification
- Rich tooling for grammar debugging and visualization
- Generated parser handles error recovery automatically
Trade-off: ANTLR4 Go target generates large files. Regeneration requires the ANTLR4 tool (make grammar).
Storage Names in BSON
Decision: Always use storageName (not qualifiedName) for the $Type field in BSON.
Rationale: Studio Pro uses storage names internally. Using qualified names causes TypeCacheUnknownTypeException. The two names are often identical, but critical exceptions exist (e.g., DomainModels$Entity vs DomainModels$EntityImpl).
Inverted Association Pointer Names
Decision: Preserve Mendix’s counter-intuitive naming where ParentPointer points to the FROM entity and ChildPointer points to the TO entity.
Rationale: Changing the field names would create a mapping layer that hides the actual BSON structure, making debugging harder. By keeping the original names, developers can directly compare Go structs with raw BSON output.
Trade-off: Every developer working with associations must learn the inverted semantics. Documentation and comments are essential.
Embedded Widget Templates
Decision: Embed a single conservative baseline template per widget (from the oldest supported Mendix version) and use runtime augmentation from the project’s .mpk files to adapt to version differences.
Rationale: Pluggable widgets require exact property schemas that change between Mendix versions. The Gallery widget, for example, has 23 properties in Mendix 10.24 but 33 in 11.6.3. Rather than shipping multiple templates per version, a single baseline works because:
- Augmentation adds missing properties: If the project’s
.mpkdeclares properties not in the template, they are cloned from type-matching exemplars or created from scratch. - Augmentation removes stale properties: Properties in the template but absent from the
.mpkare stripped, preventing CE0463. - The baseline must be the oldest supported version: A newer baseline may have structural changes that augmentation cannot reverse (different property types, different nesting). An older baseline is safe because augmentation only needs to add/remove properties, not restructure them.
Trade-off: The augmentation system handles property-level drift (added/removed properties) but cannot fix structural changes (renamed types, changed nesting depth). If a widget undergoes a major schema overhaul between versions, a version-specific template may be needed.
See Widget Template System and Pluggable Widget Engine for implementation details.
Catalog as SQLite
Decision: Use an in-memory SQLite database for the project metadata catalog.
Rationale: SQLite provides indexing, FTS5 full-text search, and standard SQL querying without any external dependency. The catalog is rebuilt from the MPR on each session, so persistence is not needed.
Fluent API Layer
Decision: Provide both a low-level SDK (direct struct construction) and a high-level fluent API (api/ package).
Rationale: The low-level SDK gives full control over BSON structure, which is needed for complex operations. The fluent API reduces boilerplate for common tasks and makes test code more readable.
Trade-off: Two API surfaces to maintain and document.