Model-Driven Development (MDD) is the practice of using models—not just code—as the primary artifact for building software. Instead of writing everything by hand, you design your system in diagrams and generate code from those models.
This tutorial covers the complete MDD workflow: from initial requirements to generated, runnable code.
What is Model-Driven Development?
In traditional development, you write code directly. Models (if they exist) are documentation—they describe what was built, not what should be built.
In MDD, the model is the source of truth:
Traditional:
Requirements → Code → (maybe) Diagrams for docs
Model-Driven:
Requirements → Models → Generated Code → Refinement
MDD vs. Low-Code vs. No-Code
| Approach | Primary Artifact | Target User | Flexibility |
|---|---|---|---|
| Traditional | Source code | Developers | Maximum |
| Model-Driven | UML/domain models | Developers/Architects | High |
| Low-Code | Visual builders | Citizen developers | Medium |
| No-Code | Templates/config | Business users | Limited |
MDD sits between traditional coding and low-code: you still write code, but the generator handles the boilerplate.
Why Use Model-Driven Development?
1. Faster scaffolding
Instead of manually creating folder structures, interfaces, and basic implementations, generate them from your model.
2. Consistent architecture
The generator enforces patterns. Every entity follows the same structure. Every service has the same interface style.
3. Living documentation
Your diagrams are your code (or at least, they produce it). When the model changes, the code changes—no stale documentation.
4. Early validation
Catch design mistakes before writing code. Model validation finds issues like:
- Circular dependencies
- Missing relationships
- Incomplete workflows
5. Reduced bugs
Less handwritten code means fewer places for typos, copy-paste errors, and inconsistencies.
The MDD Workflow: Step by Step
Step 1: Capture Requirements as a System Description
Before any diagrams, write a system description that captures:
- Users and roles: Who uses the system?
- Core entities: What are the main “things” in the system?
- Key workflows: What are the 3-5 most important processes?
- Integrations: What external systems does it connect to?
- Constraints: Security, performance, compliance requirements?
Example: Task Management System
## Task Management System
### Users
- Team members: create and complete tasks
- Team leads: assign tasks, view team progress
- Admins: manage teams, configure settings
### Core Entities
- Task: has title, description, status, assignee, due date
- Project: groups tasks, has members and a lead
- Team: groups users, has projects
- User: has name, email, role
### Key Workflows
1. Create and assign a task
2. Move task through status lifecycle (todo → in progress → review → done)
3. Generate weekly progress report
### Integrations
- Slack notifications for assignments
- Calendar sync for due dates
- GitHub PR linking
### Constraints
- Tasks cannot be assigned to users outside the project
- Only team leads can create projects
- Completed tasks are archived after 30 days
Step 2: Create UML Class Diagrams
Transform your entities into a class diagram:
┌─────────────────────────┐ ┌─────────────────────────┐
│ <<entity>> │ │ <<entity>> │
│ Task │ │ Project │
├─────────────────────────┤ ├─────────────────────────┤
│ - id: TaskId │ │ - id: ProjectId │
│ - title: string │ ◆────│ - name: string │
│ - description: string │ │ - leadId: UserId │
│ - status: TaskStatus │ │ - memberIds: UserId[] │
│ - assigneeId: UserId │ └─────────────────────────┘
│ - dueDate: Date │ │
│ - projectId: ProjectId │ │ belongs to
└─────────────────────────┘ ▼
┌─────────────────────────┐
│ <<entity>> │
│ Team │
├─────────────────────────┤
│ - id: TeamId │
│ - name: string │
│ - memberIds: UserId[] │
└─────────────────────────┘
│
│ members
▼
┌─────────────────────────┐
│ <<entity>> │
│ User │
├─────────────────────────┤
│ - id: UserId │
│ - name: string │
│ - email: string │
│ - role: UserRole │
└─────────────────────────┘
Also model value objects and enums:
┌─────────────────────────┐ ┌─────────────────────────┐
│ <<enumeration>> │ │ <<enumeration>> │
│ TaskStatus │ │ UserRole │
├─────────────────────────┤ ├─────────────────────────┤
│ TODO │ │ MEMBER │
│ IN_PROGRESS │ │ LEAD │
│ IN_REVIEW │ │ ADMIN │
│ DONE │ └─────────────────────────┘
│ ARCHIVED │
└─────────────────────────┘
Step 3: Model Behavior with Sequence Diagrams
For your key workflows, create sequence diagrams:
Workflow: Assign Task
TeamLead AssignTaskUseCase TaskRepository NotificationService
│ │ │ │
│── assign(taskId, userId) ────────▶│ │
│ │── findById() ─────▶ │
│ │◀─── task ─────────│ │
│ │ │ │
│ │── validate() ─────┤ │
│ │ (user in project?) │
│ │ │ │
│ │── task.assign() ──┤ │
│ │── save(task) ─────▶ │
│ │ │ │
│ │── notify(userId, task) ──────────────▶│
│ │ │ (Slack msg) │
│◀─── Result ───│ │ │
Step 4: Model State with State Machine Diagrams
For stateful entities, create state machine diagrams:
┌─────────┐
│ TODO │
└────┬────┘
│ start()
▼
┌───────────────┐
│ IN_PROGRESS │
└───────┬───────┘
│ submitForReview()
▼
┌─────────────┐
│ IN_REVIEW │◄────┐
└──────┬──────┘ │
approve() │ │ requestChanges()
▼ │
┌─────────┐ │
│ DONE │───────┘
└────┬────┘
│ archive() [after 30 days]
▼
┌──────────┐
│ ARCHIVED │
└──────────┘
Step 5: Validate the Model
Before generating code, validate your model:
Completeness Checks
- Every entity has an ID field
- Every relationship has clear ownership
- Every workflow references existing entities
- State machine covers all valid transitions
Consistency Checks
- No duplicate entity names
- No circular ownership dependencies
- Enum values match what workflows expect
- Foreign keys reference valid entities
Business Rule Checks
- Constraints are modeled (e.g., “only leads can create projects”)
- Validation logic is specified
- Edge cases are handled in state machines
Step 6: Generate Code
With a validated model, generate your codebase:
Generated Output:
├── src/
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── Task.ts
│ │ │ ├── Project.ts
│ │ │ ├── Team.ts
│ │ │ └── User.ts
│ │ ├── valueObjects/
│ │ │ ├── TaskId.ts
│ │ │ ├── TaskStatus.ts
│ │ │ └── UserRole.ts
│ │ └── events/
│ │ ├── TaskAssignedEvent.ts
│ │ └── TaskCompletedEvent.ts
│ │
│ ├── application/
│ │ ├── useCases/
│ │ │ ├── CreateTaskUseCase.ts
│ │ │ ├── AssignTaskUseCase.ts
│ │ │ └── CompleteTaskUseCase.ts
│ │ └── ports/
│ │ ├── TaskRepository.ts
│ │ └── NotificationService.ts
│ │
│ ├── adapters/
│ │ ├── persistence/
│ │ │ └── PostgresTaskRepository.ts
│ │ └── notifications/
│ │ └── SlackNotificationService.ts
│ │
│ └── infrastructure/
│ ├── http/
│ │ └── controllers/
│ │ └── TaskController.ts
│ └── config/
│ └── database.ts
│
├── tests/
│ ├── domain/
│ │ └── Task.test.ts
│ └── application/
│ └── AssignTaskUseCase.test.ts
│
└── package.json
Step 7: Extend and Refine
The generated code is a starting point. Add:
- Business logic details: The generator creates structure; you fill in the rules
- Error handling: Add try-catch, validation messages
- Logging and metrics: Add observability
- Tests: Expand on generated test stubs
- UI components: If generating frontend, add styling and interactions
MDD Best Practices
1. Start small
Don’t model your entire system upfront. Start with one bounded context or feature, generate it, validate the approach, then expand.
2. Keep models simple
If a diagram is too complex to fit on one page, it’s too complex. Split it into multiple diagrams or simplify the design.
3. Iterate frequently
Model → Generate → Review → Refine → Repeat
Don’t try to get the model perfect on the first try.
4. Version your models
Treat models like code: store them in git, review changes, tag releases.
5. Use conventions
Consistent naming and stereotypes make generation predictable:
<<entity>>for domain entities<<valueObject>>for value objects<<service>>for domain services<<useCase>>for application use cases<<port>>for interfaces<<adapter>>for implementations
Common MDD Patterns
Pattern: Entity + Repository + Use Case
Entity (domain) ←── Repository (port) ←── UseCase (application)
↑
Adapter (infrastructure)
Pattern: Event-Driven State Changes
Command → UseCase → Entity.method() → DomainEvent → EventHandler
Pattern: CQRS (Command/Query Separation)
Model commands (writes) separately from queries (reads):
Commands: Queries:
CreateTask → TaskRepository GetTasks → TaskQueryService
AssignTask → TaskRepository GetTaskById → TaskQueryService
Tools for Model-Driven Development
| Tool | Strength | Use Case |
|---|---|---|
| EcosystemCode | UML → Full-stack code | TypeScript, React, Node.js |
| PlantUML | Text-based diagrams | Documentation |
| Enterprise Architect | Enterprise modeling | Large organizations |
| StarUML | Desktop UML editor | Offline modeling |
Frequently Asked Questions
Is MDD slower than just coding?
Initially, yes—you’re creating models first. But over time, MDD is faster because:
- Less repetitive coding
- Fewer bugs to fix
- Easier refactoring (change the model, regenerate)
What about changes after generation?
You have options:
- Round-trip: Edit generated code, sync back to model (complex)
- Protected regions: Mark code sections the generator won’t overwrite
- Regenerate + merge: Use git to merge regenerated code with your changes
- Inheritance: Extend generated classes in separate files
Can I use MDD for existing projects?
Yes, but start small:
- Model one new feature or bounded context
- Generate it into a subdirectory
- Integrate with existing code via interfaces
Next Steps
- Try MDD yourself: Interactive demo
- Generate React components: Generate React from UML
- Generate clean architecture: TypeScript Clean Architecture Generator
- Learn about AI-assisted modeling: AI UML Generator
Ready to try model-driven development? Start building free.