Every model is just a drawing, until something makes it run.
There are two ways to make a model run: translate it into code up front (generation), or let an engine read and execute the model while it runs (interpretation). It sounds like a technical detail, but it determines how fast you can change things, how well you can debug, how dependent you are on a vendor and how your system performs. And the answer is less black-and-white than it seems: almost nobody does it purely one way.
In this post I cover:
- the core difference between compiling and interpreting, and how it carries over to models;
- why it is a spectrum rather than a choice, and why the same language can be run both ways;
- the pros and cons of code generation and of model interpretation;
- where well-known languages and low-code platforms sit, with a closer look at OutSystems and Mendix;
- rules of thumb for choosing.
The core: translating up front versus on the fly
The distinction comes straight from the world of programming languages. A compiler translates source code up front into something a machine can execute directly. An interpreter reads the source code at run time and executes it step by step.
A handy metaphor: a compiler is a translator who publishes a book in English before anyone reads it. An interpreter is like a live translator at a conference, translating sentence by sentence as the speaker talks. The book reads faster; the live translator can adapt when the conversation changes.
Model-driven development works exactly the same way, except the source is a model rather than text:
- Code generation (model-to-code, also called model compilation): a generator turns the model into source code or artifacts for a target platform up front, for example Java, C# or SQL. After that, the model is no longer needed to run the system.
- Model interpretation (model execution): a runtime engine reads the model itself and executes it. Change the model and the behaviour changes, without any code in between.
Both are ways to take the final step from model to running system. A generator is simply a compiler, just as javac is a compiler for Java source code. An interpreter is simply a platform, just as the Java Virtual Machine is a platform for Java bytecode.
Not a dichotomy, but a spectrum
The classic split “Java is compiled, JavaScript is interpreted” stopped being true a long time ago. Practically every modern environment combines both techniques:
- Java and C# are compiled up front, but to bytecode rather than machine code. The JVM or .NET runtime starts by interpreting that bytecode, then compiles the parts that run most often to machine code while the program runs, using a just-in-time (JIT) compiler. That is why a Java application starts relatively slowly and speeds up once it has warmed up.
- JavaScript starts out in an interpreter in V8 (Chrome, Node.js), but frequently executed code is compiled and optimised while it runs. None of the big browser engines interprets purely anymore.
- Python (CPython) first compiles source code to bytecode and then interprets it. Since Python 3.13 there is an experimental JIT.
- SQL is declarative: the database compiles your query into an execution plan and then executes that plan.
A quick word on two terms. Machine code consists of instructions for one specific processor, such as x86 or ARM. The processor executes it directly, but it only runs on that type of processor. Bytecode consists of instructions for a virtual machine, such as the JVM or .NET’s runtime. No real processor understands it, so a runtime interprets it or JIT-compiles it to machine code. The payoff is portability: the same Java .class file runs anywhere there is a JVM. Bytecode is a halfway point, compiled away from the source code but not yet tied to the hardware.
So the real question is not whether translation happens, but when (up front or at run time) and how many layers sit between the model and the processor.
Compiled or interpreted is a property of the implementation, not the language
The same language can be run both ways. PHP, long seen as purely interpreted, now caches compiled code and has a JIT. JavaScript runs on an interpreter plus JIT in V8, but Hermes, the engine behind React Native, compiles it ahead of time to bytecode. So “X is an interpreted language” really means “the most common implementation of X interprets”.
Who compiles the compiler?
A compiler or interpreter is itself a program, so it too has to be compiled or interpreted. GCC is written in C and compiled by an earlier version of itself, and interpreters like CPython and V8 are compiled C and C++. So every interpreted program ultimately rests on compiled code, and every hybrid is a stack of such layers. A Mendix microflow, for instance, is interpreted by a runtime written in Java, which runs on the JVM, which is itself compiled C++. Whether something counts as compiled or interpreted therefore depends on which layer you look at.
Code generation: strong on control and speed
Generation moves the translation work to before the system runs. Above all, that buys predictability.
Advantages
- Performance. Translation happens once, during the build. At run time there is no overhead for reading the model, and the generator can see the whole model and optimise across it.
- Early error detection. Inconsistencies in the model or in the transformation surface during generation and compilation, not when a user clicks a button.
- Inspectable output. Generated code can be read, reviewed, tested and debugged with standard tools.
- Less dependency. Generated code can, in principle, live on without the generator. That limits vendor lock-in, provided the code is readable.
- Extensible. Developers can place hand-written code next to the generated code for edge cases the model does not cover.
Disadvantages
- Slower feedback loop. Every change means generating, compiling and deploying again.
- The synchronisation problem. If someone edits the generated code by hand, it drifts away from the model. Patterns such as the generation gap (generated and hand-written code in separate classes) help, but do not solve it completely.
- Two levels of abstraction to understand. Whoever debugs is looking at generated code they did not write, and has to reason back to the model.
- Generators are not automatically simple. A good generator, with readable output, incremental generation and traceability back to the model, is serious work.
Model interpretation: strong on agility
With interpretation, the model itself is the program. That makes the model the single source of truth, at run time too.
Advantages
- Fast feedback. Change the model and see the effect immediately, without a build and deploy step. Some platforms even allow changes while the system is running.
- No synchronisation problem. There is no generated code that can drift from the model. What you see is what runs.
- Debugging at model level. A good interpreter lets you step through the model, in the language of the domain rather than in generated code.
- Quick to get started. For a small domain-specific language, a first interpreter is often quicker to build than a good generator. That is why Markus Völter, one of the best-known voices in this field, often suggests starting with one. The advantage fades as the language grows, as the next list shows.
Disadvantages
- Runtime overhead. The model has to be read and evaluated while the system runs. The larger the gap between model and platform, the more work that costs per action. Good caching and JIT techniques narrow the difference, but rarely close it entirely.
- Complexity grows with the language. An interpreter can only do what its engine supports. Every new language feature, optimisation or integration has to be built into the engine itself, and must work for every possible model at run time. A generator can lean on the target language for much of that: its compiler, libraries and tooling. As a DSL matures, the interpreter often becomes the more complex and more limiting of the two.
- Errors surface later. What a generator rejects at build time, an interpreter often only discovers at run time, unless the platform itself validates up front.
- Implicit choices inside the engine. An interpreter has to decide how abstract concepts are executed technically, such as storage, UI and authorisation. Those choices live in the engine, invisible and unchangeable for the modeller.
- Stronger lock-in. Without the engine, the model does not run. You depend on the runtime vendor, including for performance, security and longevity.
Classification: where do languages and platforms sit?
The table below places well-known languages and platforms on the spectrum. Look mainly at the two middle columns: what happens up front, and what happens at run time? Within each group, rows run from mostly compiled at the top to mostly interpreted at the bottom.
| Language or platform | Up front (build time) | While running (run time) | Position |
|---|---|---|---|
| Programming languages | |||
| C, C++, Rust, Go | Compiled to machine code | Processor executes machine code directly | Compilation |
| Java, C# | Compiled to bytecode | JVM or .NET runtime interprets and JIT-compiles | Hybrid, compilation-heavy |
| TypeScript | Compiler (tsc) checks types, then translates to JavaScript and erases the types | The resulting JavaScript runs in a JavaScript engine; Deno, Bun and recent Node.js strip types on the fly | Compilation to JavaScript, then hybrid |
| SQL | Query becomes an execution plan | Database executes the plan | Hybrid |
| JavaScript (V8) | Nothing; source is at most bundled or minified | Interpreter plus JIT for hot code | Hybrid, interpretation-heavy |
| PHP | Nothing separately | Compiled to opcodes on first use, cached and executed; JIT since PHP 8 | Hybrid, interpretation-heavy |
| Python (CPython) | Automatically to bytecode | Interpreter; experimental JIT since 3.13 | Interpretation |
| Model-driven platforms and tools | |||
| Generators such as JHipster, Xtext, MPS | Model or DSL becomes Java, TypeScript, etc. | Ordinary code runs | Code generation |
| MERODE (KU Leuven) | Code generator turns the domain model into a Java prototype or a RESTful application | Generated code runs; the model is embedded so behaviour can be traced back to it | Code generation |
| OutSystems | Platform generates C#/.NET plus JavaScript/React and compiles it | Compiled code runs on OutSystems runtime libraries and platform services | Code generation, platform-bound |
| Mendix | Java proxies generated and compiled; pages and nanoflows emitted as JavaScript definitions | Runtime interprets microflows; client core interprets pages and nanoflows | Hybrid, interpretation-heavy |
| Salesforce (objects, Flow) | Metadata is stored | Runtime engine executes the metadata | Model interpretation |
Put on one line, the pattern is clear: pure compilation and pure interpretation are the exceptions, and the middle is crowded.

In the low-code market, OutSystems and Mendix make an interesting pair.
OutSystems generates standard C#/.NET and JavaScript/React code, yet you still need its platform to run it. The platform compiles and deploys the code, supplies runtime libraries and adds services such as timers, processes and monitoring. Generation removes the need for a model interpreter, not the need for a runtime.
Mendix markets itself as model interpretation, but is a hybrid in practice. Microflows, pages and nanoflows are interpreted by its runtime and client. Where the model meets code, Mendix generates: Java proxy classes for entities are compiled together with custom Java actions.
Both vendors use their choice as a selling point: Mendix stresses agility and no synchronisation problems, OutSystems stresses performance and an exit strategy. Those are exactly the pros and cons mentioned before.
When to choose what?
There is no universal winner. Choose generation when performance, early error detection or independence from a runtime matter most, or when you expect your modelling language to grow large. Choose interpretation when fast or live changes, business users editing the model, or a quick prototype matter more. In practice, many successful systems are hybrid: they generate where the gap is large and performance counts, and interpret where agility counts, just as Java does with bytecode and a JIT.
Conclusion
Generation and interpretation do not differ in what happens, but in when: translating up front or while running. Generation wins on performance, early error detection and independence. Interpretation wins on agility, ease of change and debugging at model level.
The most interesting systems do not choose, they combine: generate where the distance between model and platform is large, and interpret where change needs to be fast. So if you are selecting a low-code platform or designing your own model-driven toolchain, start by asking where that distance is largest, and where change matters most.
How does your platform handle this? I would love to hear about it in the comments, or get in touch if you want to discuss your own model-driven approach.
Further reading
- Stahl, T., & Völter, M. (2006). Model-Driven Software Development: Technology, Engineering, Management. Wiley. The standard work on generative versus interpretive approaches.
- Den Haan, J. (2010). Model driven development: code generation or model interpretation? A practitioner’s view from Mendix co-founder Johan den Haan.

Comments (0)