2024-11-20
Metadata-First Manifesto: Why PMFA vs Traditional Frameworks
Question: Why write 60 lines of YAML when you could write 2,000 lines of TypeScript?
Answer: Because those 60 lines are metadata, not code.
The Problem: Code is Expensive
Traditional Approach (Spring Boot / .NET / Django)
Task: Implement Purchase Order workflow with 4 approval states.
What you write:
// Entity definition (50 lines)
@Entity()
class PurchaseOrder {
@PrimaryKey()
id: number;
@Column()
orderNumber: string;
@Column({ type: 'decimal' })
amount: number;
@Column()
status: OrderStatus;
@ManyToOne(() => Vendor)
vendor: Vendor;
@OneToMany(() => OrderLine)
lines: OrderLine[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// Repository (30 lines)
@Injectable()
class PurchaseOrderRepository {
constructor(private db: Database) {}
async findById(id: number) {
return this.db.query('SELECT * FROM purchase_orders WHERE id = $1', [id]);
}
async save(order: PurchaseOrder) {
// INSERT or UPDATE logic...
}
}
// Service layer (80 lines)
@Injectable()
class PurchaseOrderService {
constructor(private repo: PurchaseOrderRepository) {}
async submitForApproval(id: number, userId: number) {
const order = await this.repo.findById(id);
if (order.status !== 'draft') {
throw new Error('Can only submit draft orders');
}
if (order.amount > 10000 && !order.hasFinancialApproval) {
throw new Error('Financial approval required');
}
order.status = 'pending_approval';
order.submittedBy = userId;
order.submittedAt = new Date();
await this.repo.save(order);
await this.notifyApprovers(order);
}
async approve(id: number, userId: number) {
// ... 40 more lines of workflow logic
}
async reject(id: number, userId: number, reason: string) {
// ... 30 more lines
}
}
// REST API (60 lines)
@Controller('/api/purchase-orders')
class PurchaseOrderController {
constructor(private service: PurchaseOrderService) {}
@Post('/')
async create(@Body() dto: CreatePurchaseOrderDto) {
return this.service.create(dto);
}
@Get('/:id')
async getById(@Param('id') id: number) {
return this.service.findById(id);
}
@Post('/:id/submit')
async submit(@Param('id') id: number, @User() user: User) {
return this.service.submitForApproval(id, user.id);
}
@Post('/:id/approve')
async approve(@Param('id') id: number, @User() user: User) {
return this.service.approve(id, user.id);
}
}
// Frontend form (120 lines of React/Vue)
function PurchaseOrderForm() {
const [order, setOrder] = useState({});
const [errors, setErrors] = useState({});
const handleSubmit = async () => {
// Validation logic
if (!order.vendor) {
setErrors({ vendor: 'Required' });
return;
}
if (order.amount <= 0) {
setErrors({ amount: 'Must be positive' });
return;
}
await api.post('/purchase-orders', order);
};
return (
<form onSubmit={handleSubmit}>
<input name="orderNumber" ... />
<select name="vendor" ... />
<input name="amount" type="number" ... />
{/* ... 80 more lines */}
</form>
);
}
Total: ~350 lines of code
Time to implement: 2-3 days
Time to change workflow: 4-6 hours + testing
Technical debt: High (logic scattered across layers)
PMFA Approach: Metadata-First
Same Purchase Order workflow:
# meta_model.yaml (60 lines)
PurchaseOrder:
fields:
- order_number:
type: string
required: true
unique: true
pattern: "PO-\\d{6}"
- vendor_id:
type: reference(Vendor)
required: true
- amount:
type: decimal
required: true
validation: "amount > 0"
- status:
type: enum
values: [draft, pending_approval, approved, rejected]
default: draft
relations:
- lines:
type: has_many(PurchaseOrderLine)
cascade_delete: true
workflow:
states:
- draft:
transitions:
- to: pending_approval
action: submit
conditions:
- "amount <= 10000 OR financial_approval_exists"
notifications:
- send_to: approvers
template: purchase_order_submitted
- pending_approval:
transitions:
- to: approved
action: approve
required_role: [manager, director]
- to: rejected
action: reject
required_role: [manager, director]
requires_reason: true
- approved:
final: true
- rejected:
final: true
ui:
form:
layout: two_column
sections:
- title: "Order Details"
fields: [order_number, vendor_id, amount]
- title: "Line Items"
component: order_lines_table
list:
columns: [order_number, vendor.name, amount, status, created_at]
filters: [status, vendor_id, date_range]
actions: [submit, approve, reject]
permissions:
create: [employee, manager]
read: [employee, manager, director]
update: [employee] # Only draft orders
delete: [manager]
approve: [manager, director]
Total: 60 lines of YAML
What PMFA generates automatically:
- ✅ Database schema (CREATE TABLE + indexes)
- ✅ TypeScript types
- ✅ REST API endpoints (CRUD + custom actions)
- ✅ Validation rules
- ✅ Workflow state machine
- ✅ Frontend forms (React components)
- ✅ Permission checks
- ✅ Notification triggers
Time to implement: 15-30 minutes
Time to change workflow: 5 minutes (edit YAML + redeploy)
Technical debt: Zero (logic is declarative metadata)
Metadata vs Code: Key Differences
1. Declarative vs Imperative
Code (Imperative):
async function submitPurchaseOrder(id: number) {
const order = await repo.findById(id);
if (order.status !== 'draft') {
throw new Error('Invalid state');
}
if (order.amount > 10000) {
const hasApproval = await checkFinancialApproval(order);
if (!hasApproval) {
throw new Error('Financial approval required');
}
}
order.status = 'pending_approval';
await repo.save(order);
await notifyApprovers(order);
}
Metadata (Declarative):
workflow:
- from: draft
to: pending_approval
action: submit
conditions:
- "amount <= 10000 OR financial_approval_exists"
notifications:
- approvers
Difference:
- Code says HOW to do it (step-by-step)
- Metadata says WHAT should happen (rules)
PMFA runtime executes metadata. Same engine handles all workflows.
2. Change Impact
Code:
- Change workflow → rewrite service methods
- Add validation → update multiple layers
- Change UI → rewrite React components
- Result: 4-6 hours + testing
Metadata:
- Change workflow → edit YAML (2 minutes)
- Add validation → add one line (30 seconds)
- Change UI → update layout config (1 minute)
- Result: 5 minutes + automatic redeploy
3. Versioning & Rollback
Code:
# Rollback requires:
git revert abc123
npm run build
npm run test
docker build
kubectl apply
# Time: 30-60 minutes
Metadata:
# Rollback is instant:
pmfa deploy --version 1.2.3 # Previous version
# Time: 5 seconds
Real-World Example: CRM Module (3 Days vs 3 Months)
Traditional Development
Timeline:
- Week 1-2: Database schema design + Entity classes
- Week 3-4: Service layer + business logic
- Week 5-6: REST API controllers
- Week 7-8: Frontend forms + validation
- Week 9-10: Workflow state machine
- Week 11-12: Testing + bug fixes
Total: 3 months, 4 developers
Result:
- 15,000 lines of code
- 200+ unit tests
- 50+ integration tests
- Technical debt grows with every change
PMFA Development
Day 1 (Friday evening):
# Define 3 entities: Contact, Deal, Task
Contact:
fields:
- name: string
- email: string
- phone: string
- company_id: reference(Company)
Deal:
fields:
- title: string
- amount: decimal
- probability: integer[0-100]
- stage: enum[lead, qualified, proposal, won, lost]
workflow:
states: [lead, qualified, proposal, negotiation, won, lost]
Task:
fields:
- title: string
- due_date: date
- assignee_id: reference(User)
- status: enum[pending, in_progress, done]
workflow:
states: [pending, in_progress, done]
Day 2 (Saturday morning):
# Generate code
$ pmfa generate --from meta_model.yaml
Generated:
✅ 12 database tables
✅ 45 TypeScript interfaces
✅ 36 REST API endpoints
✅ 18 React form components
✅ 3 workflow engines
✅ 127 unit tests (auto-generated)
# Deploy
$ pmfa deploy --environment staging
Deployed in 12 seconds.
Day 3 (Sunday):
// Write custom business logic (only unique requirements)
PMFA.registerHook('Deal.onStateChange', async (deal, newStage) => {
if (newStage === 'won') {
await createInvoice(deal);
await notifyFinanceTeam(deal);
}
});
Total: 3 days, 1 developer
Result:
- 600 lines of YAML metadata
- 200 lines of custom TypeScript (edge cases only)
- Zero technical debt (standard PMFA patterns)
FAQ: Metadata-First Architecture
Q: What about complex business logic?
A: PMFA handles 80% automatically. You write custom code for remaining 20%.
Example:
# PMFA handles standard workflow (80% of logic)
Invoice:
workflow:
states: [draft, pending, approved, paid]
# You write custom logic (20%)
PMFA.registerValidator('Invoice.beforeApprove', async (invoice) => {
// Your unique business rule
const budget = await checkDepartmentBudget(invoice.department_id);
if (invoice.amount > budget.remaining) {
throw new Error('Exceeds budget');
}
});
Result: Best of both worlds - automatic generation + custom logic.
Q: Can I migrate existing code to PMFA?
A: Yes, gradually.
Migration strategy:
- Extract entity definitions → YAML
- Extract workflow logic → YAML workflow
- Keep custom business logic in TypeScript
- Deploy side-by-side (old + new)
- Switch traffic incrementally
Time: 2-4 weeks per module (vs 3 months rewrite from scratch)
Q: What if PMFA doesn't support my use case?
A: Extend PMFA runtime.
PMFA is open for extension:
// Register custom field type
PMFA.registerFieldType('geo_location', {
dbType: 'POINT',
validation: (value) => isValidCoordinate(value),
serialization: (value) => `(${value.lat},${value.lng})`
});
// Use in YAML
Asset:
fields:
- location: geo_location
Conclusion
Traditional code:
- ❌ 350+ lines for simple workflow
- ❌ 2-3 days to implement
- ❌ 4-6 hours to change
- ❌ Technical debt grows
PMFA metadata:
- ✅ 60 lines for same workflow
- ✅ 15-30 minutes to implement
- ✅ 5 minutes to change
- ✅ Zero technical debt
The secret: PMFA treats business logic as data, not code. When workflow is YAML, changes are configuration updates, not code rewrites.
Real-world impact:
- 🚀 10x faster development (3 days vs 3 months)
- 💰 90% cost reduction (1 dev vs 4 devs × 3 months)
- ⚡ 100x faster changes (5 min vs 6 hours)
Want to move from code to metadata? Contact us at office@nonnotech.com for PMFA technical demo.