Migration Guide to Business-as-Code
Step-by-step strategies for migrating from traditional architectures to Business-as-Code
Comprehensive strategies for transforming existing applications into autonomous Business-as-Code systems.
Overview
Migrating to Business-as-Code is a journey, not a destination. This guide provides practical strategies for teams at different starting points, whether you're working with a monolith, microservices, serverless, or building from scratch.
Assessment
Evaluating Current Architecture
Before migrating, understand what you're migrating from:
Architecture Inventory
// tools/assessment/inventory.ts
import { db } from 'sdk.do'
export class ArchitectureInventory {
async analyze() {
const inventory = {
services: await this.discoverServices(),
databases: await this.discoverDatabases(),
apis: await this.discoverAPIs(),
dependencies: await this.mapDependencies(),
businessLogic: await this.extractBusinessLogic(),
}
return {
...inventory,
complexity: this.calculateComplexity(inventory),
migrationCandidates: this.identifyMigrationCandidates(inventory),
}
}
private async discoverServices() {
// Scan codebase for service definitions
const services = []
// Example: Detect Express/Fastify/Hono apps
const appFiles = await glob('**/{server,app,index}.{ts,js}')
for (const file of appFiles) {
const content = await readFile(file, 'utf-8')
if (content.includes('express()') || content.includes('fastify()')) {
services.push({
name: path.basename(path.dirname(file)),
path: file,
type: 'http',
framework: content.includes('express') ? 'express' : 'fastify',
})
}
}
return services
}
private async discoverDatabases() {
// Find database connections
const databases = []
const configFiles = await glob('**/{config,database}.{ts,js,json}')
for (const file of configFiles) {
const content = await readFile(file, 'utf-8')
// Detect database types
if (content.includes('postgres') || content.includes('postgresql')) {
databases.push({ type: 'postgresql', config: file })
}
if (content.includes('mongodb')) {
databases.push({ type: 'mongodb', config: file })
}
if (content.includes('mysql')) {
databases.push({ type: 'mysql', config: file })
}
}
return databases
}
private async discoverAPIs() {
// Map API endpoints
const apis = []
const routeFiles = await glob('**/{routes,controllers}/**/*.{ts,js}')
for (const file of routeFiles) {
const content = await readFile(file, 'utf-8')
// Extract route definitions
const routes = this.extractRoutes(content)
apis.push(...routes.map((route) => ({ ...route, file })))
}
return apis
}
private async mapDependencies() {
// Analyze package.json dependencies
const packageJson = JSON.parse(await readFile('package.json', 'utf-8'))
return {
production: Object.keys(packageJson.dependencies || {}),
development: Object.keys(packageJson.devDependencies || {}),
total: Object.keys({
...packageJson.dependencies,
...packageJson.devDependencies,
}).length,
}
}
private async extractBusinessLogic() {
// Identify business logic patterns
const businessLogic = []
const files = await glob('src/**/*.{ts,js}')
for (const file of files) {
const content = await readFile(file, 'utf-8')
// Look for business logic indicators
if (content.includes('class') && (content.includes('Service') || content.includes('Manager') || content.includes('Handler'))) {
businessLogic.push({
file,
type: 'class',
name: this.extractClassName(content),
})
}
}
return businessLogic
}
private calculateComplexity(inventory: any) {
// Complexity score (0-100)
let score = 0
// More services = higher complexity
score += Math.min(inventory.services.length * 5, 30)
// More databases = higher complexity
score += Math.min(inventory.databases.length * 10, 20)
// More APIs = moderate complexity
score += Math.min(inventory.apis.length * 0.5, 20)
// More dependencies = higher complexity
score += Math.min(inventory.dependencies.total * 0.2, 30)
return Math.min(score, 100)
}
private identifyMigrationCandidates(inventory: any) {
// Rank services by migration priority
const candidates = inventory.services.map((service) => {
let score = 0
// Stateless services are easier to migrate
if (!service.hasState) score += 30
// Services with fewer dependencies are easier
const deps = inventory.dependencies.production.filter((dep) => service.imports?.includes(dep))
score += Math.max(0, 30 - deps.length * 2)
// Services with clear business logic are easier
const logic = inventory.businessLogic.filter((bl) => bl.file.includes(service.path))
score += Math.min(logic.length * 10, 40)
return {
...service,
migrationScore: score,
recommended: score > 50,
}
})
return candidates.sort((a, b) => b.migrationScore - a.migrationScore)
}
private extractRoutes(content: string): Array<any> {
// Simple route extraction (expand for production)
const routes = []
const routeRegex = /\.(get|post|put|patch|delete)\(['"]([^'"]+)['"]/g
let match
while ((match = routeRegex.exec(content)) !== null) {
routes.push({
method: match[1].toUpperCase(),
path: match[2],
})
}
return routes
}
private extractClassName(content: string): string {
const classMatch = content.match(/class\s+(\w+)/)
return classMatch ? classMatch[1] : 'Unknown'
}
}
// Usage
const inventory = new ArchitectureInventory()
const analysis = await inventory.analyze()
console.log('Architecture Analysis:')
console.log(`- Services: ${analysis.services.length}`)
console.log(`- Databases: ${analysis.databases.length}`)
console.log(`- APIs: ${analysis.apis.length}`)
console.log(`- Complexity Score: ${analysis.complexity}/100`)
console.log(`\nTop Migration Candidates:`)
analysis.migrationCandidates.slice(0, 5).forEach((candidate) => {
console.log(` - ${candidate.name} (score: ${candidate.migrationScore}/100)`)
})Business Logic Analysis
Identify core business logic vs infrastructure code:
// tools/assessment/business-logic.ts
export class BusinessLogicAnalyzer {
async analyze(codebase: string) {
const analysis = {
businessRules: await this.findBusinessRules(codebase),
workflows: await this.findWorkflows(codebase),
dataModels: await this.findDataModels(codebase),
integrations: await this.findIntegrations(codebase),
}
return {
...analysis,
report: this.generateReport(analysis),
}
}
private async findBusinessRules(codebase: string) {
// Look for validation, calculation, decision logic
const rules = []
const files = await glob(`${codebase}/**/*.{ts,js}`)
for (const file of files) {
const content = await readFile(file, 'utf-8')
// Find validation rules
if (content.includes('validate') || content.includes('check')) {
rules.push({
type: 'validation',
file,
examples: this.extractValidationRules(content),
})
}
// Find calculation logic
if (content.includes('calculate') || content.includes('compute')) {
rules.push({
type: 'calculation',
file,
examples: this.extractCalculations(content),
})
}
// Find business decisions
if (content.includes('decide') || content.includes('determine')) {
rules.push({
type: 'decision',
file,
examples: this.extractDecisions(content),
})
}
}
return rules
}
private async findWorkflows(codebase: string) {
// Identify multi-step processes
const workflows = []
const files = await glob(`${codebase}/**/*.{ts,js}`)
for (const file of files) {
const content = await readFile(file, 'utf-8')
// Look for async workflows
const asyncFunctions = content.match(/async\s+function\s+(\w+)/g) || []
for (const func of asyncFunctions) {
const funcName = func.replace(/async\s+function\s+/, '')
const funcBody = this.extractFunctionBody(content, funcName)
// Count steps (await calls)
const steps = (funcBody.match(/await/g) || []).length
if (steps >= 3) {
// Multi-step workflow
workflows.push({
name: funcName,
file,
steps,
canBecomeBaCWorkflow: true,
})
}
}
}
return workflows
}
private async findDataModels(codebase: string) {
// Find entity definitions
const models = []
const files = await glob(`${codebase}/**/{models,entities,types}/**/*.{ts,js}`)
for (const file of files) {
const content = await readFile(file, 'utf-8')
// TypeScript interfaces/types
const interfaces = content.match(/interface\s+(\w+)/g) || []
const types = content.match(/type\s+(\w+)\s*=/g) || []
// Classes
const classes = content.match(/class\s+(\w+)/g) || []
models.push({
file,
interfaces: interfaces.length,
types: types.length,
classes: classes.length,
canMapToSchemaOrg: this.checkSchemaOrgMapping(content),
})
}
return models
}
private async findIntegrations(codebase: string) {
// Identify external service integrations
const integrations = []
const files = await glob(`${codebase}/**/*.{ts,js}`)
for (const file of files) {
const content = await readFile(file, 'utf-8')
// Look for HTTP clients
if (content.includes('fetch(') || content.includes('axios') || content.includes('request')) {
const urls = content.match(/https?:\/\/[^\s'"]+/g) || []
integrations.push({
file,
type: 'http',
endpoints: urls,
})
}
// Look for SDK usage
const sdkPatterns = ['stripe', 'twilio', 'sendgrid', 'aws', 'gcp']
for (const sdk of sdkPatterns) {
if (content.includes(sdk)) {
integrations.push({
file,
type: 'sdk',
service: sdk,
})
}
}
}
return integrations
}
private checkSchemaOrgMapping(content: string): boolean {
// Check if entity names match Schema.org types
const schemaOrgTypes = ['Person', 'Organization', 'Product', 'Order', 'Invoice', 'Event', 'Place', 'Offer']
return schemaOrgTypes.some((type) => content.includes(type))
}
private extractFunctionBody(content: string, funcName: string): string {
// Simple extraction - expand for production
const start = content.indexOf(`function ${funcName}`)
if (start === -1) return ''
let braceCount = 0
let inFunction = false
let body = ''
for (let i = start; i < content.length; i++) {
const char = content[i]
if (char === '{') {
braceCount++
inFunction = true
}
if (inFunction) {
body += char
}
if (char === '}') {
braceCount--
if (braceCount === 0) break
}
}
return body
}
private extractValidationRules(content: string): string[] {
// Extract validation examples
return []
}
private extractCalculations(content: string): string[] {
// Extract calculation examples
return []
}
private extractDecisions(content: string): string[] {
// Extract decision logic examples
return []
}
private generateReport(analysis: any) {
return {
summary: `Found ${analysis.businessRules.length} business rules, ${analysis.workflows.length} workflows, ${analysis.dataModels.length} data models, and ${analysis.integrations.length} integrations`,
recommendations: this.generateRecommendations(analysis),
}
}
private generateRecommendations(analysis: any) {
const recommendations = []
// Workflow recommendations
const complexWorkflows = analysis.workflows.filter((w) => w.steps >= 5)
if (complexWorkflows.length > 0) {
recommendations.push({
priority: 'high',
category: 'workflows',
message: `${complexWorkflows.length} complex workflows are good candidates for Business-as-Code event-driven patterns`,
})
}
// Data model recommendations
const schemaOrgMappable = analysis.dataModels.filter((m) => m.canMapToSchemaOrg)
if (schemaOrgMappable.length > 0) {
recommendations.push({
priority: 'medium',
category: 'data-models',
message: `${schemaOrgMappable.length} data models can be mapped to Schema.org types`,
})
}
// Integration recommendations
if (analysis.integrations.length > 10) {
recommendations.push({
priority: 'high',
category: 'integrations',
message: `${analysis.integrations.length} integrations should be abstracted into semantic operations`,
})
}
return recommendations
}
}Identifying Migration Candidates
Create a decision framework:
Planning Approach
Choose your migration strategy based on:
| Factor | Greenfield | Brownfield | Hybrid |
|---|---|---|---|
| Existing Users | None | Many | Many |
| Technical Debt | N/A | High | Medium-High |
| Time Constraints | Flexible | Tight | Medium |
| Risk Tolerance | High | Low | Medium |
| Team Size | Any | Large | Medium-Large |
| Business Continuity | New product | Critical | Critical |
Migration Strategies
Greenfield Approach
Building from scratch with Business-as-Code:
Advantages
- Clean slate architecture
- No technical debt
- Modern patterns from day one
- Faster initial development
When to Use
- New products or features
- Proof of concepts
- Startups without existing code
- Failed legacy systems requiring rewrite
Implementation
// greenfield/bootstrap.ts
import $, { db, on, send, ai } from 'sdk.do'
// 1. Define business entities using Schema.org
const business = await $.Organization.create({
$type: 'Organization',
name: 'New Business',
foundingDate: new Date(),
industry: $.Industry.Technology,
})
// 2. Define core workflows
on.Customer.created(async (customer) => {
// Welcome workflow
send.Email.send({
to: customer.email,
template: 'welcome',
data: { customer },
})
// Create default preferences
$.CustomerPreferences.create({
customer,
notifications: true,
newsletter: true,
})
// Assign to onboarding agent
send.Agent.assign({
agent: 'onboarding',
customer,
})
})
// 3. Define autonomous agents
const salesAgent = await $.Agent.create({
$type: 'Agent',
name: 'Sales Agent',
role: 'sales',
capabilities: ['lead-qualification', 'product-recommendation', 'quote-generation'],
model: 'gpt-5',
})
// 4. Configure integrations
await $.Integration.create({
$type: 'Integration',
provider: 'stripe',
credentials: process.env.STRIPE_KEY,
events: ['payment.succeeded', 'payment.failed'],
})
// 5. Set up monitoring
await $.Monitor.create({
$type: 'Monitor',
metrics: ['revenue', 'conversions', 'errors'],
alerts: ['high-error-rate', 'low-conversion'],
})Brownfield Incremental Migration
Gradually transforming existing systems:
Advantages
- Lower risk
- Continuous operation
- Learn as you go
- Parallel systems during transition
When to Use
- Production systems with users
- High-risk migrations
- Limited development resources
- Need to maintain existing features
Strangler Fig Pattern
Replace system piece by piece:
Implementation:
// brownfield/router.ts
import { Hono } from 'hono'
import $, { db } from 'sdk.do'
const app = new Hono()
// Feature flags to control migration
const isFeatureMigrated = async (feature: string) => {
const [flag] = await db.FeatureFlag.query({
name: feature,
enabled: true,
})
return !!flag
}
// Route to Business-as-Code or legacy
app.get('/orders/:id', async (c) => {
const orderId = c.req.param('id')
if (await isFeatureMigrated('orders')) {
// New: Business-as-Code
const order = await db.Order.get(orderId)
return c.json(order)
} else {
// Legacy: Forward to old system
const response = await fetch(`${process.env.LEGACY_API}/orders/${orderId}`)
return c.json(await response.json())
}
})
app.post('/orders', async (c) => {
const data = await c.req.json()
if (await isFeatureMigrated('order-creation')) {
// New: Business-as-Code workflow
const order = await $.Order.create(data)
// Trigger workflow
await send('$.Order.created', order)
return c.json(order, 201)
} else {
// Legacy: Forward to old system
const response = await fetch(`${process.env.LEGACY_API}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return c.json(await response.json(), response.status)
}
})Data Migration Pattern
// brownfield/data-migration.ts
export class DataMigrator {
// Migrate data in batches
async migrateEntity(sourceTable: string, targetType: string, batchSize: number = 1000) {
let offset = 0
let hasMore = true
while (hasMore) {
// Read from legacy database
const batch = await this.readLegacyData(sourceTable, offset, batchSize)
if (batch.length === 0) {
hasMore = false
break
}
// Transform to Business-as-Code format
const transformed = batch.map((row) => this.transformRow(row, targetType))
// Write to Business-as-Code database
await db.batchCreate(targetType, transformed)
logger.info(`Migrated batch`, {
table: sourceTable,
offset,
count: batch.length,
})
offset += batchSize
}
}
private async readLegacyData(table: string, offset: number, limit: number): Promise<any[]> {
// Connect to legacy database
const result = await legacyDb.query(`SELECT * FROM ${table} LIMIT ${limit} OFFSET ${offset}`)
return result.rows
}
private transformRow(row: any, targetType: string): any {
// Transform legacy schema to Schema.org
switch (targetType) {
case '$.Customer':
return {
$type: 'Customer',
email: row.email,
givenName: row.first_name,
familyName: row.last_name,
telephone: row.phone,
// Map legacy fields to Schema.org properties
dateCreated: row.created_at,
identifier: row.legacy_id, // Preserve legacy ID for reference
}
case '$.Order':
return {
$type: 'Order',
orderNumber: row.order_number,
orderDate: row.created_at,
orderStatus: this.mapOrderStatus(row.status),
totalPrice: row.total,
priceCurrency: row.currency || 'USD',
identifier: row.legacy_id,
}
default:
throw new Error(`Unknown target type: ${targetType}`)
}
}
private mapOrderStatus(legacyStatus: string): string {
const statusMap = {
pending: 'OrderProcessing',
paid: 'OrderPaymentDue',
shipped: 'OrderInTransit',
delivered: 'OrderDelivered',
cancelled: 'OrderCancelled',
}
return statusMap[legacyStatus] || 'OrderProcessing'
}
// Dual-write pattern during migration
async dualWrite(entity: string, data: any) {
// Write to both systems
const [legacyResult, bacResult] = await Promise.all([this.writeLegacy(entity, data), this.writeBusinessAsCode(entity, data)])
// Verify consistency
if (!this.areConsistent(legacyResult, bacResult)) {
logger.error('Inconsistent write', {
entity,
legacy: legacyResult,
bac: bacResult,
})
// Alert for manual review
await alertManager.sendAlert({
severity: AlertSeverity.ERROR,
message: 'Dual write inconsistency detected',
metadata: { entity, legacyResult, bacResult },
})
}
return bacResult
}
private async writeLegacy(entity: string, data: any) {
// Write to legacy system
return await fetch(`${process.env.LEGACY_API}/${entity}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
}
private async writeBusinessAsCode(entity: string, data: any) {
// Write to Business-as-Code
return await db[entity].create(data)
}
private areConsistent(legacy: any, bac: any): boolean {
// Compare key fields
// Implement based on entity type
return true
}
}
// Usage
const migrator = new DataMigrator()
// Migrate customers
await migrator.migrateEntity('customers', '$.Customer')
// Migrate orders
await migrator.migrateEntity('orders', '$.Order')Hybrid Approach
Run both systems in parallel:
Advantages
- Lowest risk
- Easy rollback
- A/B testing possible
- Gradual user migration
When to Use
- Critical production systems
- Regulatory requirements for gradual change
- Large user bases
- Uncertainty about new architecture
Implementation
// hybrid/traffic-splitter.ts
export class TrafficSplitter {
private splitPercentage: number = 0 // Start with 0% to new system
async routeRequest(request: Request): Promise<Response> {
const shouldUseBusinessAsCode = this.shouldRoute(request)
if (shouldUseBusinessAsCode) {
return this.routeToBusinessAsCode(request)
} else {
return this.routeToLegacy(request)
}
}
private shouldRoute(request: Request): boolean {
// Route based on multiple factors
// 1. Feature flag for specific user
const userId = this.extractUserId(request)
if (userId && this.isUserInBeta(userId)) {
return true
}
// 2. Percentage-based routing
const random = Math.random() * 100
if (random < this.splitPercentage) {
return true
}
// 3. Specific endpoints already migrated
const path = new URL(request.url).pathname
if (this.isMigratedEndpoint(path)) {
return true
}
return false
}
private async routeToBusinessAsCode(request: Request): Promise<Response> {
logger.info('Routing to Business-as-Code', {
path: request.url,
method: request.method,
})
try {
// Process with Business-as-Code
const response = await this.handleBusinessAsCode(request)
// Log metrics
await this.logMetrics('business-as-code', request, response)
return response
} catch (error) {
logger.error('Business-as-Code error, falling back to legacy', error)
// Fallback to legacy on error
return this.routeToLegacy(request)
}
}
private async routeToLegacy(request: Request): Promise<Response> {
logger.info('Routing to legacy system', {
path: request.url,
method: request.method,
})
const response = await fetch(`${process.env.LEGACY_API}${new URL(request.url).pathname}`, {
method: request.method,
headers: request.headers,
body: request.body,
})
await this.logMetrics('legacy', request, response)
return response
}
// Gradually increase traffic to new system
async increaseSplit(newPercentage: number) {
if (newPercentage < 0 || newPercentage > 100) {
throw new Error('Percentage must be between 0 and 100')
}
// Check error rates before increasing
const errorRate = await this.getErrorRate('business-as-code')
if (errorRate > 0.05) {
throw new Error(`Error rate too high (${errorRate}), cannot increase traffic`)
}
this.splitPercentage = newPercentage
logger.info('Updated traffic split', {
newPercentage,
errorRate,
})
// Store in database for persistence
await db.upsert('$.TrafficSplit', {
service: 'business-as-code',
percentage: newPercentage,
updatedAt: new Date(),
})
}
private async getErrorRate(system: string): Promise<number> {
const metrics = await db.query('$.Metric', {
system,
name: 'error_rate',
'timestamp:gte': new Date(Date.now() - 3600000), // Last hour
})
if (metrics.length === 0) return 0
const totalRequests = metrics.reduce((sum, m) => sum + m.requests, 0)
const totalErrors = metrics.reduce((sum, m) => sum + m.errors, 0)
return totalErrors / totalRequests
}
}
// Gradual rollout schedule
const rollout = new TrafficSplitter()
// Week 1: 5%
await rollout.increaseSplit(5)
// Week 2: 10%
await rollout.increaseSplit(10)
// Week 3: 25%
await rollout.increaseSplit(25)
// Week 4: 50%
await rollout.increaseSplit(50)
// Week 5: 75%
await rollout.increaseSplit(75)
// Week 6: 100%
await rollout.increaseSplit(100)Step-by-Step Migration
Extract Business Logic into Functions
Transform procedural code into semantic functions:
Before: Procedural Code
// legacy/order-processing.ts
export async function processOrder(orderId: string) {
// Get order from database
const order = await db.query('SELECT * FROM orders WHERE id = ?', [orderId])
if (!order) {
throw new Error('Order not found')
}
// Check inventory
const items = await db.query('SELECT * FROM order_items WHERE order_id = ?', [orderId])
for (const item of items) {
const product = await db.query('SELECT * FROM products WHERE id = ?', [item.product_id])
if (product.stock < item.quantity) {
throw new Error(`Insufficient stock for product ${product.name}`)
}
}
// Process payment
const payment = await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
body: JSON.stringify({
amount: order.total * 100,
currency: 'usd',
source: order.payment_token,
}),
})
if (!payment.ok) {
throw new Error('Payment failed')
}
// Update inventory
for (const item of items) {
await db.query('UPDATE products SET stock = stock - ? WHERE id = ?', [item.quantity, item.product_id])
}
// Send confirmation email
await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDGRID_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: order.customer_email,
from: '[email protected]',
subject: 'Order Confirmation',
html: `Your order ${order.order_number} has been confirmed!`,
}),
})
// Update order status
await db.query('UPDATE orders SET status = ? WHERE id = ?', ['processed', orderId])
}After: Business-as-Code
// business-as-code/order-processing.ts
import $, { db, send, on } from 'sdk.do'
// Semantic workflow
on.Order.created(async (order: Order) => {
// 1. Validate inventory (semantic operation)
const validation = await send.Inventory.validate({
items: order.orderedItem,
})
if (!validation.valid) {
send.Order.cancel({
order,
reason: validation.reason,
})
return
}
// 2. Process payment (semantic operation)
const payment = await send.Payment.process({
order,
amount: order.totalPrice,
})
if (payment.status !== 'succeeded') {
send.Order.cancel({
order,
reason: 'Payment failed',
})
return
}
// 3. Reserve inventory (semantic operation)
send.Inventory.reserve({
items: order.orderedItem,
})
// 4. Send confirmation (semantic operation)
send.Email.send({
to: order.customer.email,
template: 'order-confirmation',
data: { order },
})
// 5. Update status (semantic operation)
db.Order.update(order.$id, {
orderStatus: 'OrderProcessing',
})
})
// Individual semantic functions
$.Payment.process.implement(async ({ order, amount }) => {
// Stripe integration abstracted
const stripe = await $.Integration.get('stripe')
return stripe.charge({
amount,
currency: order.priceCurrency,
source: order.paymentMethod,
})
})
$.Inventory.validate.implement(async ({ items }) => {
for (const item of items) {
const product = await db.Product.get(item.orderedItem.$id)
if (product.quantityAvailable < item.orderQuantity) {
return {
valid: false,
reason: `Insufficient inventory for ${product.name}`,
}
}
}
return { valid: true }
})
$.Inventory.reserve.implement(async ({ items }) => {
for (const item of items) {
db.Product.update(item.orderedItem.$id, {
quantityAvailable: (product) => product.quantityAvailable - item.orderQuantity,
})
}
})Convert Workflows to Declarative Definitions
Transform imperative workflows into declarative event-driven patterns:
// business-as-code/workflows/customer-onboarding.ts
import $, { on, send, db, ai } from 'sdk.do'
// Declarative workflow definition
export const customerOnboardingWorkflow = {
name: 'Customer Onboarding',
trigger: $.Customer.created,
steps: [
{
name: 'send-welcome-email',
action: $.Email.send,
input: (customer) => ({
to: customer.email,
template: 'welcome',
data: { customer },
}),
},
{
name: 'create-default-preferences',
action: $.CustomerPreferences.create,
input: (customer) => ({
customer,
notifications: true,
newsletter: true,
}),
},
{
name: 'ai-personalization',
action: ai.personalize,
input: (customer) => ({
customer,
recommendations: true,
}),
},
{
name: 'assign-account-manager',
action: $.Agent.assign,
input: (customer) => ({
agent: 'account-manager',
customer,
}),
condition: (customer) => customer.tier === 'enterprise',
},
],
onError: {
action: $.Task.create,
input: (customer, error) => ({
title: 'Onboarding Failed',
description: `Customer ${customer.email} onboarding failed: ${error.message}`,
assignedTo: 'support-team',
}),
},
}
// Register workflow
on(customerOnboardingWorkflow.trigger, async (customer) => {
for (const step of customerOnboardingWorkflow.steps) {
// Check condition
if (step.condition && !step.condition(customer)) {
continue
}
try {
await send(step.action, step.input(customer))
} catch (error) {
await send(customerOnboardingWorkflow.onError.action, customerOnboardingWorkflow.onError.input(customer, error))
throw error
}
}
})Implement Event-Driven Patterns
Replace synchronous calls with asynchronous events:
Implementation:
// business-as-code/event-driven/order-api.ts
import { Hono } from 'hono'
import $, { db, send } from 'sdk.do'
const app = new Hono()
// Synchronous: Return immediately after creating order
app.post('/orders', async (c) => {
const data = await c.req.json()
// Create order
const order = await $.Order.create({
...data,
orderStatus: 'OrderProcessing',
orderDate: new Date(),
})
// Emit event (async processing)
await send('$.Order.created', order)
// Return immediately
return c.json(
{
order,
message: 'Order created and will be processed asynchronously',
},
202 // Accepted
)
})
// Client can poll or use webhooks for updates
app.get('/orders/:id', async (c) => {
const order = await db.Order.get(c.req.param('id'))
return c.json(order)
})
// Webhook for status updates
on.Order.statusChanged(async (order) => {
if (order.webhookUrl) {
fetch(order.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'order.status_changed',
order,
}),
})
}
})Deploy Autonomous Agents
Replace manual processes with AI agents:
// business-as-code/agents/customer-support.ts
import $, { db, ai, send } from 'sdk.do'
export class CustomerSupportAgent {
async handleTicket(ticket: Ticket) {
// 1. Classify ticket
const classification = await ai.classify({
text: ticket.description,
categories: ['technical', 'billing', 'product', 'urgent'],
})
// 2. Search knowledge base
const knowledgeBase = await db.search('$.Article', {
query: ticket.description,
limit: 5,
})
// 3. Generate response
const response = await ai.generate({
model: 'gpt-5',
prompt: 'Provide customer support response',
context: {
ticket,
classification,
knowledgeBase,
},
})
// 4. Decide if human intervention needed
if (response.confidence < 0.8 || classification.category === 'urgent') {
// Escalate to human
await send('$.Task.create', {
title: `Customer Support: ${ticket.subject}`,
description: ticket.description,
assignedTo: 'support-team',
priority: classification.category === 'urgent' ? 'high' : 'normal',
metadata: {
ticketId: ticket.$id,
suggestedResponse: response.text,
},
})
} else {
// Send automated response
await send('$.Email.send', {
to: ticket.customer.email,
subject: `Re: ${ticket.subject}`,
body: response.text,
})
// Update ticket
db.Ticket.update(ticket.$id, {
status: 'resolved',
resolution: response.text,
resolvedBy: 'ai-agent',
resolvedAt: new Date(),
})
}
}
}Migrate Data Models
Map legacy schemas to Schema.org types:
// migration/schema-mapper.ts
export class SchemaMapper {
// Map legacy User to Schema.org Person
legacyUserToPerson(legacyUser: LegacyUser): Person {
return {
$type: 'Person',
givenName: legacyUser.first_name,
familyName: legacyUser.last_name,
email: legacyUser.email,
telephone: legacyUser.phone,
birthDate: legacyUser.date_of_birth,
address: {
$type: 'PostalAddress',
streetAddress: legacyUser.address,
addressLocality: legacyUser.city,
addressRegion: legacyUser.state,
postalCode: legacyUser.zip,
addressCountry: legacyUser.country,
},
// Preserve legacy ID
identifier: legacyUser.id,
}
}
// Map legacy Product to Schema.org Product
legacyProductToProduct(legacyProduct: LegacyProduct): Product {
return {
$type: 'Product',
name: legacyProduct.name,
description: legacyProduct.description,
sku: legacyProduct.sku,
brand: {
$type: 'Brand',
name: legacyProduct.brand_name,
},
offers: [
{
$type: 'Offer',
price: legacyProduct.price,
priceCurrency: legacyProduct.currency || 'USD',
availability: this.mapAvailability(legacyProduct.in_stock),
},
],
image: legacyProduct.image_url,
category: legacyProduct.category,
quantityAvailable: legacyProduct.stock_quantity,
identifier: legacyProduct.id,
}
}
private mapAvailability(inStock: boolean): string {
return inStock ? 'InStock' : 'OutOfStock'
}
}Integration Patterns
Legacy System Integration
Connect Business-as-Code with legacy systems:
// integration/legacy-adapter.ts
import $, { db } from 'sdk.do'
export class LegacyAdapter {
// Read from legacy system
async syncFromLegacy(entity: string) {
const legacyData = await fetch(`${process.env.LEGACY_API}/${entity}`)
const data = await legacyData.json()
for (const item of data) {
// Check if already exists
const existing = await db[entity].query({
identifier: item.id,
})
if (existing.length === 0) {
// Create new
await db[entity].create(this.transform(item, entity))
} else {
// Update existing
await db[entity].update(existing[0].$id, this.transform(item, entity))
}
}
}
// Write to legacy system
async syncToLegacy(entity: string, bacEntity: any) {
const legacyFormat = this.transformToLegacy(bacEntity, entity)
await fetch(`${process.env.LEGACY_API}/${entity}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(legacyFormat),
})
}
private transform(legacyData: any, entity: string): any {
// Transform legacy format to Business-as-Code format
switch (entity) {
case 'Customer':
return {
$type: 'Customer',
email: legacyData.email,
givenName: legacyData.first_name,
familyName: legacyData.last_name,
identifier: legacyData.id,
}
default:
return legacyData
}
}
private transformToLegacy(bacEntity: any, entity: string): any {
// Transform Business-as-Code format to legacy format
switch (entity) {
case 'Customer':
return {
id: bacEntity.identifier,
email: bacEntity.email,
first_name: bacEntity.givenName,
last_name: bacEntity.familyName,
}
default:
return bacEntity
}
}
}API Bridging
Create adapter layer:
// integration/api-bridge.ts
import { Hono } from 'hono'
import $, { db } from 'sdk.do'
const app = new Hono()
// Legacy API format -> Business-as-Code
app.post('/legacy/users', async (c) => {
const legacyUser = await c.req.json()
// Transform to Business-as-Code Person
const person = await $.Person.create({
givenName: legacyUser.first_name,
familyName: legacyUser.last_name,
email: legacyUser.email,
identifier: legacyUser.user_id,
})
// Return in legacy format
return c.json({
user_id: person.identifier,
first_name: person.givenName,
last_name: person.familyName,
email: person.email,
})
})
// Business-as-Code -> Legacy API format
app.get('/legacy/users/:id', async (c) => {
const [person] = await db.Person.query({
identifier: c.req.param('id'),
})
if (!person) {
return c.json({ error: 'User not found' }, 404)
}
// Return in legacy format
return c.json({
user_id: person.identifier,
first_name: person.givenName,
last_name: person.familyName,
email: person.email,
})
})Data Synchronization
Keep systems in sync during migration:
// integration/data-sync.ts
import $, { on, send, db } from 'sdk.do'
export class DataSync {
// Bi-directional sync
async initialize() {
// Business-as-Code -> Legacy
on.Customer.created(this.syncCustomerToLegacy.bind(this))
on.Customer.updated(this.syncCustomerToLegacy.bind(this))
// Legacy -> Business-as-Code (via webhook or polling)
this.pollLegacyChanges()
}
private async syncCustomerToLegacy(customer: Customer) {
try {
fetch(`${process.env.LEGACY_API}/customers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: customer.identifier,
email: customer.email,
first_name: customer.givenName,
last_name: customer.familyName,
}),
})
} catch (error) {
logger.error('Failed to sync customer to legacy', error)
// Queue for retry
db.SyncQueue.create({
$type: 'SyncQueue',
entity: 'Customer',
entityId: customer.$id,
direction: 'to_legacy',
retryCount: 0,
})
}
}
private async pollLegacyChanges() {
setInterval(async () => {
try {
const response = await fetch(`${process.env.LEGACY_API}/customers/changes`)
const changes = await response.json()
for (const change of changes) {
await this.applyChange(change)
}
} catch (error) {
logger.error('Failed to poll legacy changes', error)
}
}, 60000) // Poll every minute
}
private async applyChange(change: any) {
switch (change.type) {
case 'customer_created':
case 'customer_updated':
const [existing] = await db.Customer.query({
identifier: change.data.id,
})
if (existing) {
db.Customer.update(existing.$id, {
email: change.data.email,
givenName: change.data.first_name,
familyName: change.data.last_name,
})
} else {
await $.Customer.create({
identifier: change.data.id,
email: change.data.email,
givenName: change.data.first_name,
familyName: change.data.last_name,
})
}
break
}
}
}Event Streaming
Stream events between systems:
// integration/event-stream.ts
import $, { send } from 'sdk.do'
export class EventStreamBridge {
// Kafka/RabbitMQ -> Business-as-Code
async consumeExternalEvents() {
const consumer = kafka.consumer({ groupId: 'business-as-code' })
await consumer.connect()
await consumer.subscribe({ topic: 'legacy-events' })
await consumer.run({
eachMessage: async ({ message }) => {
const event = JSON.parse(message.value.toString())
// Transform and emit as Business-as-Code event
await this.transformAndEmit(event)
},
})
}
// Business-as-Code -> Kafka/RabbitMQ
async produceBusinessEvents() {
on.Order.created(async (order) => {
kafka.producer().send({
topic: 'business-events',
messages: [
{
key: order.$id,
value: JSON.stringify({
type: 'order.created',
data: order,
timestamp: Date.now(),
}),
},
],
})
})
}
private async transformAndEmit(externalEvent: any) {
switch (externalEvent.type) {
case 'user.created':
await send('$.Customer.created', {
$type: 'Customer',
email: externalEvent.data.email,
givenName: externalEvent.data.firstName,
familyName: externalEvent.data.lastName,
})
break
case 'order.placed':
await send('$.Order.created', {
$type: 'Order',
orderNumber: externalEvent.data.orderNumber,
totalPrice: externalEvent.data.total,
orderStatus: 'OrderProcessing',
})
break
}
}
}Webhook Patterns
Legacy systems notify Business-as-Code via webhooks:
// integration/webhook-receiver.ts
import { Hono } from 'hono'
import $, { send } from 'sdk.do'
const app = new Hono()
// Receive webhooks from legacy system
app.post('/webhooks/legacy/:event', async (c) => {
const event = c.req.param('event')
const data = await c.req.json()
// Verify webhook signature
if (!this.verifySignature(c.req.header('x-signature'), data)) {
return c.json({ error: 'Invalid signature' }, 401)
}
// Transform to Business-as-Code event
await this.handleLegacyEvent(event, data)
return c.json({ received: true })
})
private async handleLegacyEvent(event: string, data: any) {
switch (event) {
case 'payment_succeeded':
await send('$.Payment.completed', {
$type: 'Payment',
amount: data.amount / 100,
priceCurrency: data.currency,
paymentStatus: 'PaymentComplete',
})
break
case 'user_registered':
await send('$.Customer.created', {
$type: 'Customer',
email: data.email,
givenName: data.first_name,
familyName: data.last_name,
})
break
}
}
private verifySignature(signature: string, data: any): boolean {
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(data))
.digest('hex')
return signature === expectedSignature
}Testing During Migration
Parallel Running
Run both systems and compare results:
// testing/parallel-runner.ts
export class ParallelRunner {
async compareImplementations(request: Request): Promise<ComparisonResult> {
// Run both systems
const [legacyResult, bacResult] = await Promise.all([this.runLegacy(request), this.runBusinessAsCode(request)])
// Compare results
const comparison = {
match: this.resultsMatch(legacyResult, bacResult),
legacy: legacyResult,
businessAsCode: bacResult,
differences: this.findDifferences(legacyResult, bacResult),
}
// Log comparison
await this.logComparison(comparison)
// Alert on mismatch
if (!comparison.match) {
await alertManager.sendAlert({
severity: AlertSeverity.WARNING,
message: 'Implementation mismatch detected',
metadata: comparison,
})
}
// Return Business-as-Code result (or legacy if safer)
return comparison.match ? bacResult : legacyResult
}
private async runLegacy(request: Request) {
const start = Date.now()
try {
const response = await fetch(`${process.env.LEGACY_API}${request.url}`, {
method: request.method,
headers: request.headers,
body: request.body,
})
return {
success: true,
data: await response.json(),
duration: Date.now() - start,
}
} catch (error) {
return {
success: false,
error: error.message,
duration: Date.now() - start,
}
}
}
private async runBusinessAsCode(request: Request) {
const start = Date.now()
try {
const result = await this.handleBusinessAsCodeRequest(request)
return {
success: true,
data: result,
duration: Date.now() - start,
}
} catch (error) {
return {
success: false,
error: error.message,
duration: Date.now() - start,
}
}
}
private resultsMatch(legacy: any, bac: any): boolean {
if (legacy.success !== bac.success) return false
// Deep comparison of data
return JSON.stringify(legacy.data) === JSON.stringify(bac.data)
}
private findDifferences(legacy: any, bac: any): Difference[] {
const differences: Difference[] = []
// Compare each field
const legacyKeys = Object.keys(legacy.data || {})
const bacKeys = Object.keys(bac.data || {})
for (const key of new Set([...legacyKeys, ...bacKeys])) {
if (legacy.data[key] !== bac.data[key]) {
differences.push({
field: key,
legacy: legacy.data[key],
businessAsCode: bac.data[key],
})
}
}
return differences
}
}Shadow Testing
Test new system without affecting users:
// testing/shadow-testing.ts
export class ShadowTester {
async shadowTest(request: Request): Promise<Response> {
// Process request with legacy system (real)
const legacyResponse = await this.processLegacy(request)
// Process same request with Business-as-Code (shadow)
// Run async, don't wait
this.processShadow(request).catch((error) => {
logger.error('Shadow test error', error)
})
// Return legacy response
return legacyResponse
}
private async processShadow(request: Request) {
try {
const bacResponse = await this.processBusinessAsCode(request)
// Compare with legacy (would need to store legacy result)
await this.compareAndLog(request, bacResponse)
} catch (error) {
// Log shadow test failures
db.ShadowTestFailure.create({
$type: 'ShadowTestFailure',
request: {
url: request.url,
method: request.method,
},
error: error.message,
timestamp: new Date(),
})
}
}
}A/B Testing
Compare systems with real users:
// testing/ab-testing.ts
export class ABTester {
async test(request: Request): Promise<Response> {
const userId = this.extractUserId(request)
const variant = this.assignVariant(userId)
if (variant === 'a') {
// Legacy system
return this.processLegacy(request)
} else {
// Business-as-Code system
return this.processBusinessAsCode(request)
}
}
private assignVariant(userId: string): 'a' | 'b' {
// Consistent assignment based on user ID
const hash = crypto.createHash('md5').update(userId).digest('hex')
const value = parseInt(hash.substring(0, 8), 16)
return value % 2 === 0 ? 'a' : 'b'
}
// Track metrics by variant
async trackMetric(userId: string, metric: string, value: number) {
const variant = this.assignVariant(userId)
db.ABTestMetric.create({
$type: 'ABTestMetric',
variant,
metric,
value,
timestamp: new Date(),
})
}
// Analyze results
async analyzeResults() {
const metrics = await db.ABTestMetric.query({})
const byVariant = {
a: metrics.filter((m) => m.variant === 'a'),
b: metrics.filter((m) => m.variant === 'b'),
}
return {
variantA: this.calculateStats(byVariant.a),
variantB: this.calculateStats(byVariant.b),
significant: this.isStatisticallySignificant(byVariant.a, byVariant.b),
}
}
private calculateStats(metrics: any[]) {
const values = metrics.map((m) => m.value)
return {
count: values.length,
mean: values.reduce((a, b) => a + b, 0) / values.length,
min: Math.min(...values),
max: Math.max(...values),
}
}
private isStatisticallySignificant(a: any[], b: any[]): boolean {
// T-test or other statistical test
// Simplified for example
return Math.abs(this.calculateStats(a).mean - this.calculateStats(b).mean) > 0.1
}
}Rollback Strategies
Quick rollback if issues detected:
// testing/rollback.ts
export class MigrationRollback {
async checkHealthAndRollback() {
const health = await this.checkSystemHealth()
if (!health.healthy) {
logger.error('System unhealthy, initiating rollback', health)
await this.rollback()
}
}
private async checkSystemHealth() {
const checks = await Promise.all([this.checkErrorRate(), this.checkResponseTime(), this.checkDataIntegrity()])
return {
healthy: checks.every((c) => c.passed),
checks,
}
}
private async checkErrorRate(): Promise<HealthCheck> {
const errorRate = await this.getErrorRate()
return {
name: 'error_rate',
passed: errorRate < 0.05,
value: errorRate,
threshold: 0.05,
}
}
private async checkResponseTime(): Promise<HealthCheck> {
const p95 = await this.getP95ResponseTime()
return {
name: 'response_time_p95',
passed: p95 < 1000,
value: p95,
threshold: 1000,
}
}
private async checkDataIntegrity(): Promise<HealthCheck> {
// Compare row counts, checksums, etc.
const legacyCount = await this.getLegacyCount()
const bacCount = await this.getBusinessAsCodeCount()
const difference = Math.abs(legacyCount - bacCount) / legacyCount
return {
name: 'data_integrity',
passed: difference < 0.01, // Less than 1% difference
value: difference,
threshold: 0.01,
}
}
private async rollback() {
// Switch traffic back to legacy
await this.switchTrafficToLegacy()
// Alert team
await alertManager.sendAlert({
severity: AlertSeverity.CRITICAL,
message: 'Migration rolled back due to health check failures',
})
// Create incident
db.Incident.create({
$type: 'Incident',
title: 'Migration Rollback',
severity: 'critical',
timestamp: new Date(),
})
}
}Case Studies
Monolith to Business-as-Code
Company: E-commerce Platform Before: Rails monolith, 500K LOC After: Business-as-Code autonomous business Timeline: 12 months Team: 15 developers
Challenges
- 10-year-old codebase with significant technical debt
- 5M active users requiring zero downtime
- Complex checkout workflow with 20+ steps
- Integration with 30+ third-party services
Approach
-
Assessment (Month 1-2)
- Mapped 200 API endpoints
- Identified 15 core business workflows
- Extracted 50 business entities
-
Foundation (Month 3-4)
- Set up Business-as-Code infrastructure
- Mapped data models to Schema.org
- Created integration adapters
-
Incremental Migration (Month 5-10)
- Migrated one workflow per sprint
- Started with new features (greenfield)
- Moved to read-only endpoints
- Finally migrated write operations
-
Cutover (Month 11-12)
- Ran parallel systems for 2 months
- Gradually shifted traffic
- Decommissioned monolith
Results
- Performance: 60% faster response times
- Development Speed: 3x faster feature development
- Costs: 40% reduction in infrastructure costs
- Quality: 80% reduction in bugs
- Team Satisfaction: Significantly improved
Microservices to Business-as-Code
Company: SaaS Platform Before: 40 microservices, complex orchestration After: Unified Business-as-Code system Timeline: 9 months Team: 20 developers
Challenges
- Service sprawl with unclear boundaries
- Complex inter-service communication
- Duplicate business logic across services
- Difficult to maintain consistency
Approach
-
Consolidation (Month 1-3)
- Grouped services by business domain
- Identified shared business logic
- Created semantic interface layer
-
Semantic Layer (Month 4-6)
- Defined semantic operations ($.Subject.predicate.Object)
- Implemented semantic routing
- Migrated services to semantic patterns
-
Simplification (Month 7-9)
- Replaced complex orchestration with events
- Unified data models with Schema.org
- Deployed autonomous agents
Results
- Services: Reduced from 40 to 5 semantic domains
- Complexity: 70% reduction in inter-service calls
- Consistency: Unified business logic
- Observability: Single semantic trace
- Onboarding: New developers productive in days vs weeks
Serverless to Business-as-Code
Company: FinTech Startup Before: 100+ AWS Lambda functions After: Business-as-Code autonomous finance platform Timeline: 6 months Team: 8 developers
Challenges
- Function sprawl and naming inconsistency
- Cold start latency issues
- Complex IAM and permission management
- Difficult local development and testing
Approach
-
Function Mapping (Month 1-2)
- Mapped functions to semantic operations
- Identified shared patterns
- Grouped by business capability
-
Semantic Functions (Month 3-4)
- Implemented $.Subject.predicate.Object pattern
- Created semantic function registry
- Added intelligent routing
-
Platform Migration (Month 5-6)
- Migrated to Business-as-Code runtime
- Implemented edge deployment
- Added AI-driven optimization
Results
- Functions: Reduced from 100+ to 20 semantic operations
- Latency: Zero cold starts
- Development: Unified local dev experience
- Testing: Comprehensive test coverage
- Costs: 50% reduction in compute costs
Summary
Migration to Business-as-Code is a journey that requires:
-
Thorough Assessment: Understand your current architecture and identify migration candidates
-
Right Strategy: Choose greenfield, brownfield, or hybrid based on your situation
-
Step-by-Step Approach: Extract business logic, convert workflows, implement events, deploy agents
-
Integration Patterns: Bridge legacy systems with adapters, synchronization, and event streaming
-
Continuous Testing: Use parallel running, shadow testing, and A/B testing to ensure reliability
-
Quick Rollback: Have rollback procedures ready for when issues arise
Next Steps
- Start Small: Begin with a single feature or workflow
- Measure Everything: Track metrics throughout migration
- Iterate: Learn from each step and adjust approach
- Communicate: Keep team and stakeholders informed
- Celebrate: Recognize milestones and successes
For production best practices, see Best Practices Guide