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:

  1. Extract entity definitions → YAML
  2. Extract workflow logic → YAML workflow
  3. Keep custom business logic in TypeScript
  4. Deploy side-by-side (old + new)
  5. 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.