Developer Experience
Developer experience del MVP. Target: from
git cloneto first running workflow en < 30 minutos. Local dev: Docker Compose one-command. SDK: hello-world worker < 10 LOC. Documentation hierarchy: 5-min quickstart → 30-min tutorial → reference. Examples library: 20+ realistic workflows. Testing: unit/integration/e2e patterns. Debugging tools: process inspector + trace viewer + replay tool. CLI completo. Onboarding metrics tracked: time-to-first-workflow, time-to-production.
DX target¶
Goal: a developer who has never used the MVP should be able to:
| Milestone | Target time |
|---|---|
| Local environment running | 5 minutes |
| First workflow deployed | 15 minutes |
| First worker complete a job | 30 minutes |
| Understand the model | 2 hours (tutorial) |
| Deploy something real | 1 day |
| In production confidently | 1 week |
These are aggressive but achievable with good DX investment.
Local development setup¶
One-command quickstart¶
make quickstart should:
1. Check prerequisites (Docker, Docker Compose)
2. Pull/build images
3. Start: Postgres, engine, frontend
4. Run migrations
5. Seed test tenant + user
6. Open browser to local Inspector
Total time: < 5 minutes for first-time user with prerequisites.
Docker Compose stack¶
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: dev
POSTGRES_DB: workflow
volumes:
- pg-data:/var/lib/postgresql/data
ports: ["5432:5432"]
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
engine:
build: ./engine
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://postgres:dev@postgres/workflow
AUTH_MODE: dev # bypass OIDC in dev
ports: ["8080:8080"]
webapp:
build: ./webapp
depends_on: [engine]
ports: ["3000:3000"]
environment:
API_BASE: http://engine:8080
# Optional: keycloak for full OIDC testing
keycloak:
image: quay.io/keycloak/keycloak:24
profiles: [full]
ports: ["8090:8080"]
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
command: start-dev
volumes:
pg-data:
make quickstart runs docker compose up. make quickstart-full includes Keycloak for OIDC testing.
Hot reload¶
Engine code changes trigger rebuild + restart:
watch mode (Docker Compose v2.22+) rebuilds on file changes. Sub-30s feedback loop.
First workflow tutorial¶
Step 1: Hello World BPMN¶
Create hello.bpmn:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
targetNamespace="http://example.com/bpmn">
<bpmn:process id="hello-world" isExecutable="true">
<bpmn:startEvent id="start" />
<bpmn:sequenceFlow sourceRef="start" targetRef="say-hello" />
<bpmn:serviceTask id="say-hello" name="Say Hello">
<bpmn:extensionElements>
<zeebe:taskDefinition type="say-hello" />
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:sequenceFlow sourceRef="say-hello" targetRef="end" />
<bpmn:endEvent id="end" />
</bpmn:process>
</bpmn:definitions>
Step 2: Deploy¶
Step 3: Worker¶
// worker.ts
import { MVPClient, JobWorker } from '@mvp/sdk';
const client = new MVPClient({
endpoint: 'http://localhost:8080',
apiKey: 'dev-key'
});
const worker = new JobWorker({
client,
jobType: 'say-hello',
handler: async (job) => {
console.log('Hello, world!');
return { greeting: 'Hello!' };
}
});
worker.start();
console.log('Worker started');
Step 4: Create instance¶
mvp-cli create-instance hello-world
# Output: Created instance: 9876543210
# Worker logs immediately:
# Hello, world!
# Verify
mvp-cli instances get 9876543210
# Output: { state: 'COMPLETED', ... }
Total time: ~15 minutes for a developer following the tutorial.
SDK ergonomics¶
Goal: most common operations < 5 LOC.
Worker examples¶
// Basic worker (already shown)
new JobWorker({ client, jobType: 'foo', handler: async (job) => {} }).start();
// Worker with input/output typing (Zod)
const InputSchema = z.object({
customerId: z.string(),
amount: z.number()
});
new TypedJobWorker({
client,
jobType: 'charge-card',
schema: InputSchema,
handler: async (job, input) => {
// input.customerId is typed string
// input.amount is typed number
const result = await stripe.charge(input);
return { transactionId: result.id };
}
}).start();
// Worker with idempotency
new JobWorker({
client,
jobType: 'send-email',
idempotencyStore: new RedisStore('redis://...'),
handler: async (job) => {
// Automatically dedup based on job key
await emailProvider.send(job.variables);
}
}).start();
// Worker with retries config
new JobWorker({
client,
jobType: 'call-flaky-api',
retries: {
max: 5,
backoff: 'exponential',
initialDelayMs: 1000
},
handler: async (job) => {
// Engine retries handles retries automatically
}
}).start();
Client examples¶
// Create instance
const pi = await client.processInstances.create({
bpmnProcessId: 'hello-world',
variables: { name: 'Alice' }
});
// Wait for completion (sync mode)
const result = await client.processInstances.createAndWait({
bpmnProcessId: 'hello-world',
variables: { name: 'Alice' },
timeoutMs: 30000
});
// Search
const instances = await client.processInstances.search({
state: 'ACTIVE',
bpmnProcessId: 'hello-world'
});
// Get variables
const vars = await client.processInstances.getVariables(pi.key);
// Cancel
await client.processInstances.cancel(pi.key, { reason: 'duplicate order' });
// Publish message (correlate)
await client.messages.correlate({
name: 'payment-received',
correlationKey: 'order-123',
variables: { amount: 100 }
});
Code completion + type safety from TypeScript types generated from OpenAPI.
Documentation hierarchy¶
Layer 1: README (1 minute)¶
# MVP Workflow Engine
A simplified workflow engine inspired by Camunda 8, built on PostgreSQL.
## Quick start
\`\`\`bash
make quickstart
open http://localhost:3000
\`\`\`
[Full tutorial](./docs/tutorial.md)
Layer 2: Quickstart (5 minutes)¶
Pre-built docker compose + simple worker example.
Layer 3: Tutorial (30 minutes)¶
Step-by-step: 1. Deploy a process 2. Write a worker 3. Create instances 4. Handle user tasks 5. Use forms 6. Handle errors
Layer 4: How-to guides (per task)¶
- How to: integrate with Slack
- How to: send emails
- How to: handle long-running tasks
- How to: implement saga pattern
- How to: deploy to production
- How to: monitor workflows
Layer 5: Reference¶
- BPMN supported elements
- API reference (auto-generated)
- SDK reference (auto-generated)
- CLI reference
Layer 6: Concepts¶
- Architecture overview
- BPMN execution model
- Why event sourcing
- Tradeoffs
Layer 7: Operations¶
- Deployment guide
- Monitoring setup
- Troubleshooting
- Performance tuning
- Migration guides
Examples library¶
20+ realistic workflows showing patterns:
examples/
├── 01-hello-world/
├── 02-user-task-approval/
├── 03-parallel-gateway/
├── 04-exclusive-gateway/
├── 05-timer-events/
├── 06-error-handling/
├── 07-multi-instance/
├── 08-subprocess/
├── 09-call-activity/
├── 10-message-correlation/
├── 11-signal-broadcast/
├── 12-form-handling/
├── 13-saga-pattern/
├── 14-long-polling/
├── 15-batch-processing/
├── 16-scheduled-jobs/
├── 17-webhook-integration/
├── 18-database-transaction/
├── 19-machine-learning-pipeline/
└── 20-complex-approval-workflow/
Each example has:
- README explaining the use case
- process.bpmn BPMN file
- worker.ts worker code
- tests/ integration tests
- run.sh to execute
Testing patterns¶
Unit tests for workers¶
import { mockJob, mockClient } from '@mvp/sdk/testing';
import { handler } from './worker';
test('handles successful payment', async () => {
const job = mockJob({
variables: { amount: 100, customerId: 'cust-1' }
});
const result = await handler(job);
expect(result.transactionId).toBeDefined();
});
Integration tests (with engine)¶
import { TestEngine } from '@mvp/sdk/testing';
test('order-approval flow', async () => {
const engine = await TestEngine.start(); // in-memory engine
await engine.deploy('order-approval.bpmn');
const pi = await engine.createInstance('order-approval', {
orderId: 'order-1',
amount: 100
});
// Complete service task
const job = await engine.activateJob('check-credit');
await engine.completeJob(job.key, { approved: true });
// Complete user task
const task = await engine.findUserTask('manager-approval');
await engine.completeTask(task.key, { decision: 'approve' });
// Verify final state
const finalState = await engine.getInstance(pi.key);
expect(finalState.state).toBe('COMPLETED');
});
TestEngine is in-memory implementation. Fast (< 100ms per test).
Property-based tests¶
import { fc } from 'fast-check';
test('any valid path completes the process', () => {
fc.assert(
fc.property(
fc.bpmnModel(),
fc.executionPath(),
async (model, path) => {
const engine = await TestEngine.start();
await engine.deploy(model);
const pi = await engine.createInstance(model.processId);
for (const step of path.steps) {
await engine.execute(step, pi.key);
}
const final = await engine.getInstance(pi.key);
expect(final.state).toBe('COMPLETED');
}
)
);
});
Per concepts/property-based-testing.
E2E tests¶
import { TestCluster } from '@mvp/sdk/testing';
test('full cluster e2e', async () => {
const cluster = await TestCluster.start({
engineCount: 1,
workerCount: 2,
realDatabase: true // uses test Postgres
});
// ... full workflow execution
await cluster.stop();
});
Slower (~5-10s per test) but realistic.
Debugging tools¶
Process Inspector (built-in)¶
For local development: - Browse all instances - See current element - View variables (live) - See history - Cancel/retry from UI
Trace viewer (via APM)¶
OpenTelemetry traces show: - API → engine → worker → completion - Latency per step - Error sources
Replay tool¶
Recreate failure deterministically:
mvp-cli replay --process-instance=12345 --from-event=1
# Replays the entire process execution event by event
# Outputs state at each step
# Identifies divergence from expected
Useful for debugging "why did this fail in prod?"
Log inspection¶
mvp-cli logs --process-instance=12345 --since=1h
# Returns all log entries related to this instance
# Includes worker logs (correlated by trace_id)
CLI completo¶
# Process management
mvp-cli deploy <file.bpmn>
mvp-cli instances list [--state=ACTIVE] [--process=foo]
mvp-cli instances get <key>
mvp-cli instances create <process-id> [--variables=...]
mvp-cli instances cancel <key>
mvp-cli instances variables <key>
# Job management
mvp-cli jobs list [--type=foo] [--state=ACTIVATED]
mvp-cli jobs get <key>
# Incidents
mvp-cli incidents list [--state=ACTIVE]
mvp-cli incidents resolve <key>
# User tasks
mvp-cli tasks list [--assignee=me]
mvp-cli tasks claim <key>
mvp-cli tasks complete <key> --variables=...
# Identity
mvp-cli users list
mvp-cli users grant <user-id> <role> --tenant=<id>
mvp-cli api-keys create --name=worker --role=worker
# Admin
mvp-cli tenants list
mvp-cli tenants create <name>
mvp-cli audit search --action=...
# Debug
mvp-cli logs --process-instance=<key>
mvp-cli replay --process-instance=<key>
mvp-cli health
mvp-cli metrics
# Migration (from Camunda 8)
mvp-cli bpmn convert --from=camunda <file>
mvp-cli migrate from-camunda --camunda-url=... --target=...
All operations possible via CLI. Power users productive immediately.
Error messages¶
Bad error message:
Good error message:
Error: Validation failed for process instance creation:
- bpmnProcessId: Process 'hello-world' has no version 5 (latest is 3)
- variables.amount: Expected number, got string "100"
Hint: Use --version=latest or specify existing version (1, 2, or 3)
Documentation: https://docs.mvp.dev/troubleshooting#PROCESS_VERSION_NOT_FOUND
Each error: - What went wrong - Why (which constraint failed) - How to fix - Link to docs
IDE integration¶
VS Code extension (Phase 2 nice-to-have)¶
- Syntax highlighting for BPMN
- Inline preview of process
- Auto-complete CEL expressions
- Jump from BPMN element to worker code
- Snippets for common patterns
IntelliJ plugin¶
For Java developers migrating from Camunda.
Onboarding metrics¶
Track these to validate DX:
| Metric | Target |
|---|---|
| Time-to-first-workflow (median) | < 30 min |
| Time-to-production (median) | < 1 week |
| Documentation page views per user | 5-10 |
| Help/support tickets per user | < 1 first month |
| Net Promoter Score from devs | > 50 |
| Tutorial completion rate | > 70% |
Survey new developers at 1 week + 1 month.
Anti-patterns to avoid¶
Don't require kubernetes for local dev¶
Many devs don't have K8s locally. Docker Compose is universal.
Don't make auth complex in dev¶
AUTH_MODE: dev bypasses OIDC for local. Production uses full OIDC.
Don't bury errors in stack traces¶
Surface user-friendly errors. Stack trace available with --verbose.
Don't require API keys in tutorial¶
Dev mode uses simple bearer token. Production introduces OIDC + API keys.
Don't have multiple ways to do everything¶
One blessed path. Document alternatives clearly.
Documentation maintenance¶
Documentation rot is real. Strategies:
- Code examples in docs are tested (linkcheck + executable)
- API reference auto-generated from OpenAPI
- Version-pin docs match release version
- Stale docs flagged via timestamps
- Community contributions welcome
Marketing the developer story¶
Public-facing value props:
- 15-minute setup vs Camunda's hours
- One database vs Camunda's RocksDB + Elasticsearch
- Real-time queries vs Camunda's eventually consistent
- 80% cheaper at scale
- Open source vs Camunda's commercial license
- No JVM required vs Camunda's Java-only
DX is product. Investment here pays off in adoption.
Links¶
- analysis/implementation-roadmap-concrete — DX considerations in roadmap
- adrs/adr-016-minimal-outbound-worker-sdk — SDK design
- analysis/migration-from-camunda-8 — Onboarding from Camunda users
- concepts/test-infrastructure — Testing patterns reference