.do
ScaleServices-as-Software

Service Composition

Combining services for complex workflows

Learn how to combine multiple Services-as-Software to create powerful, complex workflows.

graph TB subgraph "Sequential Composition" S1[Service A] --> S2[Service B] S2 --> S3[Service C] S3 --> R1[Result] end subgraph "Parallel Composition" Input[Input] --> P1[Service D] Input --> P2[Service E] Input --> P3[Service F] P1 & P2 & P3 --> Combine[Combine Results] Combine --> R2[Result] end subgraph "Event-Driven Composition" E1[Event] --> SvcX[Service X] SvcX -->|Emit| E2[New Event] E2 --> SvcY[Service Y] SvcY -->|Emit| E3[Final Event] end

Composition Patterns

Sequential Composition

Execute services in sequence:

const workflow = $.workflow('content-pipeline', async (topic) => {
  // Step 1: Generate content
  const article = await services.contentGenerator.generate({
    topic,
    keywords: ['AI', 'automation'],
    length: 1500,
  })

  // Step 2: Generate images
  const images = await services.imageGenerator.generate({
    descriptions: extractImageNeeds(article),
    style: 'professional',
  })

  // Step 3: Optimize for SEO
  const optimized = await services.seoOptimizer.optimize({
    content: article,
    images,
    targetKeywords: ['AI', 'automation'],
  })

  // Step 4: Publish
  const published = await services.cms.publish({
    content: optimized,
    images,
    schedule: 'next_available',
  })

  return published
})

Parallel Composition

Execute services concurrently:

const workflow = $.workflow('lead-enrichment', async (lead) => {
  // Run multiple services in parallel
  const [companyData, socialProfiles, creditScore, techStack] = await Promise.all([
    services.companyEnrichment.enrich({ company: lead.company }),
    services.socialLookup.find({ email: lead.email }),
    services.creditCheck.score({ company: lead.company }),
    services.techDetector.detect({ domain: lead.website }),
  ])

  // Combine results
  return {
    ...lead,
    companyData,
    socialProfiles,
    creditScore,
    techStack,
  }
})

Conditional Composition

Route based on conditions:

const workflow = $.workflow('order-fulfillment', async (order) => {
  // Check inventory
  const available = await services.inventory.check(order.items)

  if (available) {
    // Standard fulfillment
    return await services.fulfillment.process({
      order,
      priority: 'standard',
    })
  } else {
    // Backorder process
    const backorder = await services.backorder.create(order)

    // Notify customer
    await services.notifications.send({
      to: order.customer,
      template: 'backorder',
      data: { backorder },
    })

    return backorder
  }
})

Event-Driven Composition

Services communicate via events:

// Service A: Order Processing
export default service({
  name: 'Order Processing',

  on: {
    '$.Order.created': async (event) => {
      // Process order
      const processed = await processOrder(event.data)

      // Emit event for next service
      await $.send('$.Order.processed', processed)
    },
  },
})

// Service B: Fulfillment
export default service({
  name: 'Fulfillment',

  on: {
    '$.Order.processed': async (event) => {
      // Fulfill order
      const shipment = await fulfillOrder(event.data)

      // Emit event for next service
      await $.send('$.Shipment.created', shipment)
    },
  },
})

// Service C: Notifications
export default service({
  name: 'Notifications',

  on: {
    '$.Shipment.created': async (event) => {
      // Notify customer
      await $.Email.send({
        to: event.data.customer.email,
        template: 'shipment-notification',
        data: event.data,
      })
    },
  },
})

Service Discovery

Install Services

Install services from marketplace:

// Install a service
const service = await $.services.install('customer-support-ai')

// Use the service
const result = await service.analyzeTicket({
  ticketId: 'ticket_123',
})

Service Registry

Query available services:

// Find services by category
const services = await $.services.find({
  category: 'marketing',
  tags: ['email', 'automation'],
})

// Get service info
const info = await $.services.info('email-marketing-pro')
console.log(info)
// {
//   name: 'Email Marketing Pro',
//   description: 'Advanced email marketing automation',
//   version: '2.1.0',
//   pricing: { model: 'subscription', plans: [...] },
//   capabilities: ['send', 'track', 'analyze']
// }

Service Dependencies

Declare Dependencies

Declare required services:

export default service({
  name: 'Content Marketing Suite',

  dependencies: {
    contentGenerator: 'content-generator@^2.0.0',
    imageGenerator: 'image-generator@^1.5.0',
    seoOptimizer: 'seo-optimizer@^3.2.0',
    cms: 'cms-publisher@^1.0.0',
  },

  api: {
    'POST /create-campaign': async (req) => {
      const { topic } = await req.json()

      // Use dependent services
      const article = await $.deps.contentGenerator.generate({ topic })
      const images = await $.deps.imageGenerator.generate({ article })
      const optimized = await $.deps.seoOptimizer.optimize({ article, images })
      const published = await $.deps.cms.publish({ content: optimized })

      return { published }
    },
  },
})

Version Compatibility

Specify version constraints:

dependencies: {
  'service-a': '2.1.0',        // Exact version
  'service-b': '^2.0.0',       // Compatible with 2.x.x
  'service-c': '~1.5.0',       // Compatible with 1.5.x
  'service-d': '>=1.0.0',      // 1.0.0 or higher
}

Data Flow Patterns

Pipeline Pattern

Process data through multiple stages:

const pipeline = $.pipeline('data-enrichment', [
  // Stage 1: Clean
  async (data) => {
    return await services.dataCleaner.clean(data)
  },

  // Stage 2: Validate
  async (data) => {
    return await services.validator.validate(data)
  },

  // Stage 3: Enrich
  async (data) => {
    return await services.enrichment.enrich(data)
  },

  // Stage 4: Score
  async (data) => {
    return await services.scorer.score(data)
  },
])

// Run pipeline
const result = await pipeline.run(rawData)

Fan-Out/Fan-In Pattern

Distribute work and aggregate results:

const workflow = $.workflow('multi-channel-campaign', async (campaign) => {
  // Fan-out: Send to multiple channels
  const results = await Promise.all([services.email.send(campaign), services.sms.send(campaign), services.push.send(campaign), services.social.post(campaign)])

  // Fan-in: Aggregate results
  const aggregated = {
    totalSent: results.reduce((sum, r) => sum + r.sent, 0),
    totalDelivered: results.reduce((sum, r) => sum + r.delivered, 0),
    byChannel: results.map((r) => ({
      channel: r.channel,
      sent: r.sent,
      delivered: r.delivered,
    })),
  }

  return aggregated
})

Request-Reply Pattern

Synchronous service calls:

// Service A: Make request
const response = await services.analyzer.analyze({
  data: inputData,
  options: { detailed: true },
})

// Service B: Process and reply
export default service({
  name: 'Analyzer',

  api: {
    'POST /analyze': async (req) => {
      const { data, options } = await req.json()
      const analysis = await performAnalysis(data, options)
      return { analysis } // Reply
    },
  },
})

Error Handling in Composition

flowchart TD Call[Service Call] --> Try{Success?} Try -->|Yes| Success[Return Result] Try -->|No| Retry{Retryable?} Retry -->|Yes| Backoff[Exponential Backoff] Backoff --> Attempt{Attempts Left?} Attempt -->|Yes| Call Attempt -->|No| Fallback{Fallback?} Retry -->|No| Fallback Fallback -->|Yes| Alt[Use Fallback Service] Fallback -->|No| Compensate[Compensating Transaction] Alt --> Success Compensate --> Error[Return Error]

Retry Logic

Retry failed service calls:

const workflow = $.workflow('resilient-process', async (data) => {
  try {
    return await services.external.process(data)
  } catch (error) {
    if (error.retryable) {
      // Retry with exponential backoff
      return await retry(() => services.external.process(data), {
        attempts: 3,
        delay: 1000,
        backoff: 2,
      })
    }
    throw error
  }
})

Fallback Services

Use fallback when primary fails:

const workflow = $.workflow('payment-processing', async (payment) => {
  try {
    // Try primary processor
    return await services.stripe.charge(payment)
  } catch (error) {
    // Fallback to secondary
    try {
      return await services.paypal.charge(payment)
    } catch (fallbackError) {
      // Both failed - alert and queue
      await $.alert('payment-processors-down', { payment })
      return await $.queue.add('pending-payments', payment)
    }
  }
})

Compensating Transactions

Rollback on failure:

const workflow = $.workflow('order-placement', async (order) => {
  let inventoryReserved = false
  let paymentCharged = false

  try {
    // Step 1: Reserve inventory
    await services.inventory.reserve(order.items)
    inventoryReserved = true

    // Step 2: Charge payment
    await services.payment.charge(order)
    paymentCharged = true

    // Step 3: Create shipment
    const shipment = await services.fulfillment.ship(order)

    return { success: true, shipment }
  } catch (error) {
    // Compensate for completed steps
    if (paymentCharged) {
      await services.payment.refund(order)
    }
    if (inventoryReserved) {
      await services.inventory.release(order.items)
    }

    throw error
  }
})

Service Orchestration

Workflow Orchestration

Coordinate complex workflows:

export default service({
  name: 'Workflow Orchestrator',

  workflows: {
    'customer-onboarding': {
      steps: [
        {
          name: 'create-account',
          service: 'auth-service',
          action: 'createAccount',
          input: '$.input.customer',
        },
        {
          name: 'setup-billing',
          service: 'billing-service',
          action: 'setupCustomer',
          input: '$.steps.create-account.output',
          dependsOn: ['create-account'],
        },
        {
          name: 'send-welcome',
          service: 'email-service',
          action: 'sendWelcome',
          input: '$.steps.create-account.output',
          dependsOn: ['create-account'],
        },
        {
          name: 'provision-resources',
          service: 'provisioning-service',
          action: 'provision',
          input: '$.steps.create-account.output',
          dependsOn: ['create-account', 'setup-billing'],
        },
      ],
    },
  },
})

Service Mesh

Manage service-to-service communication:

export default service({
  name: 'API Gateway',

  mesh: {
    services: {
      'customer-service': {
        url: 'https://customer.do',
        auth: 'jwt',
        retry: { attempts: 3 },
        timeout: 5000,
      },
      'order-service': {
        url: 'https://order.do',
        auth: 'jwt',
        retry: { attempts: 3 },
        timeout: 5000,
      },
      'payment-service': {
        url: 'https://payment.do',
        auth: 'api-key',
        retry: { attempts: 5 },
        timeout: 10000,
      },
    },
  },
})

Monitoring Composed Services

Distributed Tracing

Track requests across services:

const workflow = $.workflow('multi-service-process', async (data) => {
  // Start trace
  const trace = $.trace.start('multi-service-process')

  try {
    // Step 1
    const result1 = await trace.span('service-a', () => services.serviceA.process(data))

    // Step 2
    const result2 = await trace.span('service-b', () => services.serviceB.process(result1))

    // Step 3
    const result3 = await trace.span('service-c', () => services.serviceC.process(result2))

    trace.end({ success: true })
    return result3
  } catch (error) {
    trace.end({ success: false, error })
    throw error
  }
})

Aggregate Metrics

Monitor composed services:

// Track workflow metrics
await $.metric.histogram('workflow.duration', duration, {
  workflow: 'content-pipeline',
  status: 'success',
})

await $.metric.increment('workflow.executions', {
  workflow: 'content-pipeline',
})

// Query metrics
const metrics = await $.metric.query({
  metric: 'workflow.duration',
  workflow: 'content-pipeline',
  timeRange: 'last_24_hours',
})

Best Practices

Loose Coupling

Keep services independent:

// Good: Services communicate via events
on: {
  '$.Order.placed': async (event) => {
    await $.send('$.Fulfillment.requested', event.data)
  },
}

// Bad: Direct service dependencies
on: {
  '$.Order.placed': async (event) => {
    await services.fulfillment.process(event.data)  // Tight coupling
  },
}

Idempotency

Make compositions idempotent:

const workflow = $.workflow('idempotent-process', async (data) => {
  const key = `workflow:${data.id}`

  // Check if already processed
  const existing = await $.cache.get(key)
  if (existing) {
    return existing
  }

  // Process
  const result = await processData(data)

  // Cache result
  await $.cache.set(key, result, { ttl: 3600 })

  return result
})

Graceful Degradation

Handle service unavailability:

const workflow = $.workflow('resilient-workflow', async (data) => {
  // Try enhanced processing
  try {
    return await services.enhanced.process(data)
  } catch (error) {
    // Fall back to basic processing
    return await services.basic.process(data)
  }
})

Next Steps