.do

Service Composition

Learn how to combine services to create more powerful offerings

Learn how to combine multiple services into more powerful, valuable offerings.

Why Compose Services?

Individual services are valuable, but composed services create exponentially more value:

  • Increased Value: Bundle complementary services
  • Simplified User Experience: One request, multiple capabilities
  • Better Pricing: Package discounts and bundling
  • Enhanced Capabilities: Services working together achieve more

Learn more advanced patterns:

Composition Patterns

Sequential Composition

Services execute one after another, each using the previous service's output. See advanced sequential patterns →

import $, { db, on, send } from 'sdk.do'

// Define a sequential composition
const blogProductionService = await $.Service.create({
  name: 'Blog Production Pipeline',
  type: $.ServiceType.Composite,
  compositionType: 'sequential',

  // Define the sequence
  pipeline: [
    {
      order: 1,
      service: $.Service['Keyword Research'],
      outputs: ['keywords', 'topics'],
    },
    {
      order: 2,
      service: $.Service['Content Writer'],
      inputs: {
        keywords: { from: 1, field: 'keywords' },
        topic: { from: 1, field: 'topics[0]' },
      },
      outputs: ['content', 'title'],
    },
    {
      order: 3,
      service: $.Service['SEO Optimizer'],
      inputs: {
        content: { from: 2, field: 'content' },
        keywords: { from: 1, field: 'keywords' },
      },
      outputs: ['optimizedContent', 'seoMetadata'],
    },
    {
      order: 4,
      service: $.Service['Grammar Checker'],
      inputs: {
        text: { from: 3, field: 'optimizedContent' },
      },
      outputs: ['finalContent'],
    },
  ],

  pricing: {
    model: 'bundled',
    baseRate: 150.0,
    discount: 0.15, // 15% off individual services
  },
})

// Execute sequential pipeline
on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== blogProductionService.id) return

  const results = []

  try {
    for (const stage of blogProductionService.pipeline) {
      // Prepare inputs for this stage
      const stageInputs = prepareStageInputs(stage, results, request.inputs)

      // Execute service
      const result = await send.ServiceRequest.create({
        serviceId: stage.service.id,
        inputs: stageInputs,
        parentRequest: request.id,
      })

      // Wait for completion
      const output = await waitForServiceCompletion(result.id)

      // Store result
      results.push({
        stage: stage.order,
        service: stage.service.name,
        output,
      })

      // Update progress
      send.ServiceProgress.updated({
        requestId: request.id,
        stage: stage.order,
        total: blogProductionService.pipeline.length,
        progress: stage.order / blogProductionService.pipeline.length,
      })
    }

    // Deliver final result
    send.ServiceResult.deliver({
      requestId: request.id,
      outputs: {
        content: results[results.length - 1].output.finalContent,
        seoMetadata: results[2].output.seoMetadata,
        keywords: results[0].output.keywords,
        pipeline: results,
      },
    })
  } catch (error) {
    send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      completedStages: results.length,
      partialResults: results,
    })
  }
})

// Helper: Prepare inputs from previous stages
function prepareStageInputs(stage, previousResults, initialInputs) {
  const inputs = {}

  // Map inputs from previous stages
  for (const [key, mapping] of Object.entries(stage.inputs || {})) {
    if (mapping.from) {
      const sourceResult = previousResults[mapping.from - 1]
      inputs[key] = getNestedValue(sourceResult.output, mapping.field)
    }
  }

  // Include initial inputs if needed
  for (const [key, value] of Object.entries(initialInputs)) {
    if (!inputs[key]) {
      inputs[key] = value
    }
  }

  return inputs
}

Parallel Composition

Multiple services execute simultaneously. See advanced parallel patterns →

// Define parallel composition
const contentPackageService = await $.Service.create({
  name: 'Content Package Generator',
  type: $.ServiceType.Composite,
  compositionType: 'parallel',

  // Services run in parallel
  services: [
    {
      service: $.Service['Blog Post Writer'],
      required: true,
    },
    {
      service: $.Service['Social Media Creator'],
      required: false,
    },
    {
      service: $.Service['Email Newsletter'],
      required: false,
    },
    {
      service: $.Service['Infographic Generator'],
      required: false,
    },
  ],

  pricing: {
    model: 'per-component',
    rates: {
      blog: 100.0,
      social: 30.0,
      email: 50.0,
      infographic: 75.0,
    },
  },
})

// Execute services in parallel
on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== contentPackageService.id) return

  try {
    // Start all services concurrently
    const promises = contentPackageService.services.map(async (component) => {
      // Check if component was requested
      if (component.required || request.inputs.components?.includes(component.service.name)) {
        const result = await send.ServiceRequest.create({
          serviceId: component.service.id,
          inputs: request.inputs,
          parentRequest: request.id,
        })

        return {
          service: component.service.name,
          result: await waitForServiceCompletion(result.id),
        }
      }
      return null
    })

    // Wait for all to complete
    const results = (await Promise.all(promises)).filter((r) => r !== null)

    // Deliver combined results
    send.ServiceResult.deliver({
      requestId: request.id,
      outputs: results.reduce((acc, r) => {
        acc[r.service] = r.result
        return acc
      }, {}),
    })

    // Calculate total cost
    const cost = results.reduce((total, r) => {
      const componentKey = r.service.toLowerCase().split(' ')[0]
      return total + contentPackageService.pricing.rates[componentKey]
    }, 0)

    send.Payment.charge({
      customerId: request.customerId,
      amount: cost,
      breakdown: results.map((r) => ({
        service: r.service,
        amount: contentPackageService.pricing.rates[r.service.toLowerCase().split(' ')[0]],
      })),
    })
  } catch (error) {
    send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
    })
  }
})

Conditional Composition

Services execute based on conditions or outcomes. See event-driven patterns →

// Define conditional composition
const smartContentService = await $.Service.create({
  name: 'Smart Content Creator',
  type: $.ServiceType.Composite,
  compositionType: 'conditional',

  workflow: [
    {
      service: $.Service['Content Analyzer'],
      always: true,
    },
    {
      service: $.Service['Simple Writer'],
      condition: 'analysis.complexity === "simple"',
    },
    {
      service: $.Service['Advanced Writer'],
      condition: 'analysis.complexity === "complex"',
    },
    {
      service: $.Service['Translation'],
      condition: 'request.inputs.translate === true',
    },
    {
      service: $.Service['Quality Check'],
      always: true,
    },
  ],
})

// Execute with conditional logic
on.ServiceRequest.created(async (request) => {
  if (request.serviceId !== smartContentService.id) return

  const context = {
    request: request.inputs,
    results: {},
  }

  try {
    for (const step of smartContentService.workflow) {
      // Check if step should execute
      if (step.always || evaluateCondition(step.condition, context)) {
        const result = await send.ServiceRequest.create({
          serviceId: step.service.id,
          inputs: prepareInputs(step, context),
          parentRequest: request.id,
        })

        const output = await waitForServiceCompletion(result.id)
        context.results[step.service.name] = output

        // Update context for next steps
        if (step.service.name === 'Content Analyzer') {
          context.analysis = output
        }
      }
    }

    // Deliver final result
    send.ServiceResult.deliver({
      requestId: request.id,
      outputs: context.results,
    })
  } catch (error) {
    send.ServiceRequest.fail({
      requestId: request.id,
      error: error.message,
      context,
    })
  }
})

// Evaluate condition
function evaluateCondition(condition: string, context: any): boolean {
  // Simple condition evaluation
  // In production, use a safe expression evaluator
  try {
    const func = new Function('ctx', `with(ctx) { return ${condition} }`)
    return func(context)
  } catch {
    return false
  }
}

Hierarchical Composition

Services organized in a tree structure:

// Define hierarchical composition
const enterpriseContentService = await $.Service.create({
  name: 'Enterprise Content Suite',
  type: $.ServiceType.Composite,
  compositionType: 'hierarchical',

  structure: {
    root: {
      service: $.Service['Content Strategy'],
      children: [
        {
          service: $.Service['Blog Content'],
          children: [{ service: $.Service['Blog Writer'] }, { service: $.Service['SEO Optimizer'] }, { service: $.Service['Image Generator'] }],
        },
        {
          service: $.Service['Social Media'],
          children: [{ service: $.Service['Twitter Posts'] }, { service: $.Service['LinkedIn Posts'] }, { service: $.Service['Instagram Content'] }],
        },
        {
          service: $.Service['Email Marketing'],
          children: [{ service: $.Service['Newsletter Writer'] }, { service: $.Service['Campaign Creator'] }],
        },
      ],
    },
  },
})

// Execute hierarchically
async function executeHierarchical(node, context) {
  // Execute current service
  const result = await send.ServiceRequest.create({
    serviceId: node.service.id,
    inputs: context.inputs,
    parentRequest: context.requestId,
  })

  const output = await waitForServiceCompletion(result.id)

  // If has children, execute them with output as context
  if (node.children) {
    const childResults = await Promise.all(
      node.children.map((child) =>
        executeHierarchical(child, {
          ...context,
          inputs: { ...context.inputs, ...output },
        })
      )
    )

    return {
      service: node.service.name,
      output,
      children: childResults,
    }
  }

  return {
    service: node.service.name,
    output,
  }
}

Service Dependencies

Declaring Dependencies

Services can declare their dependencies:

const advancedWriterService = await $.Service.create({
  name: 'Advanced Content Writer',
  type: $.ServiceType.ContentGeneration,

  // Declare dependencies
  dependencies: {
    required: [$.Service['Grammar Checker']],
    recommended: [$.Service['SEO Optimizer'], $.Service['Plagiarism Checker']],
    optional: [$.Service['Image Generator'], $.Service['Translation']],
  },

  // Auto-invoke dependencies
  autoInvoke: {
    'Grammar Checker': 'always',
    'SEO Optimizer': 'if-available',
    'Plagiarism Checker': 'if-requested',
  },
})

Dependency Resolution

Automatically resolve and execute dependencies:

async function executeWithDependencies(service, request) {
  const results = {}

  // Execute required dependencies first
  for (const dep of service.dependencies.required) {
    const result = await send.ServiceRequest.create({
      serviceId: dep.id,
      inputs: request.inputs,
      parentRequest: request.id,
    })

    results[dep.name] = await waitForServiceCompletion(result.id)
  }

  // Execute main service with dependency results
  const mainResult = await service.execute({
    ...request.inputs,
    dependencies: results,
  })

  // Execute recommended dependencies if configured
  if (service.autoInvoke) {
    for (const [depName, condition] of Object.entries(service.autoInvoke)) {
      if (shouldInvoke(condition, request, mainResult)) {
        const dep = service.dependencies.recommended.find((d) => d.name === depName)
        if (dep) {
          const result = await send.ServiceRequest.create({
            serviceId: dep.id,
            inputs: { ...request.inputs, content: mainResult.output },
            parentRequest: request.id,
          })

          results[depName] = await waitForServiceCompletion(result.id)
        }
      }
    }
  }

  return {
    main: mainResult,
    dependencies: results,
  }
}

Service Orchestration

Workflow Engine

Build a workflow engine for complex compositions:

class ServiceOrchestrator {
  async execute(workflow, request) {
    const context = {
      requestId: request.id,
      inputs: request.inputs,
      results: new Map(),
      state: {},
    }

    // Execute workflow steps
    for (const step of workflow.steps) {
      try {
        const result = await this.executeStep(step, context)
        context.results.set(step.id, result)

        // Update state
        if (step.stateUpdates) {
          this.updateState(context.state, step.stateUpdates, result)
        }

        // Check exit conditions
        if (step.exitIf && this.evaluateCondition(step.exitIf, context)) {
          break
        }
      } catch (error) {
        if (step.onError === 'continue') {
          context.results.set(step.id, { error })
          continue
        } else if (step.onError === 'retry') {
          // Implement retry logic
          const retried = await this.retryStep(step, context, error)
          context.results.set(step.id, retried)
        } else {
          throw error
        }
      }
    }

    return context.results
  }

  async executeStep(step, context) {
    switch (step.type) {
      case 'service':
        return await this.executeService(step, context)
      case 'parallel':
        return await this.executeParallel(step.steps, context)
      case 'condition':
        return await this.executeConditional(step, context)
      case 'loop':
        return await this.executeLoop(step, context)
      default:
        throw new Error(`Unknown step type: ${step.type}`)
    }
  }

  async executeService(step, context) {
    const inputs = this.prepareInputs(step.inputs, context)

    const result = await send.ServiceRequest.create({
      serviceId: step.serviceId,
      inputs,
      parentRequest: context.requestId,
    })

    return await waitForServiceCompletion(result.id)
  }

  async executeParallel(steps, context) {
    const promises = steps.map((step) => this.executeStep(step, context))
    return await Promise.all(promises)
  }

  async executeConditional(step, context) {
    if (this.evaluateCondition(step.condition, context)) {
      return await this.executeStep(step.then, context)
    } else if (step.else) {
      return await this.executeStep(step.else, context)
    }
    return null
  }

  async executeLoop(step, context) {
    const results = []
    const items = this.resolveValue(step.items, context)

    for (const item of items) {
      const itemContext = { ...context, currentItem: item }
      const result = await this.executeStep(step.do, itemContext)
      results.push(result)
    }

    return results
  }
}

// Usage
const orchestrator = new ServiceOrchestrator()

const complexWorkflow = {
  steps: [
    {
      id: 'analyze',
      type: 'service',
      serviceId: $.Service['Content Analyzer'].id,
      inputs: { content: '${inputs.content}' },
    },
    {
      id: 'parallel-generation',
      type: 'parallel',
      steps: [
        {
          id: 'blog',
          type: 'service',
          serviceId: $.Service['Blog Writer'].id,
        },
        {
          id: 'social',
          type: 'service',
          serviceId: $.Service['Social Creator'].id,
        },
      ],
    },
    {
      id: 'quality-check',
      type: 'condition',
      condition: '${results.blog.quality} < 0.8',
      then: {
        type: 'service',
        serviceId: $.Service['Content Improver'].id,
      },
    },
  ],
}

const result = await orchestrator.execute(complexWorkflow, request)

Service Marketplace

Publishing Composed Services

Make your compositions available to others:

// Publish to marketplace
await $.Service.publish({
  serviceId: blogProductionService.id,
  marketplace: {
    visibility: 'public',
    category: 'Content Marketing',
    tags: ['blog', 'seo', 'content-creation'],
    featured: true,
  },
  pricing: {
    model: 'subscription',
    tiers: [
      {
        name: 'Starter',
        price: 99,
        limits: { postsPerMonth: 10 },
      },
      {
        name: 'Professional',
        price: 299,
        limits: { postsPerMonth: 50 },
      },
      {
        name: 'Enterprise',
        price: 999,
        limits: { postsPerMonth: 200 },
      },
    ],
  },
  documentation: {
    description: 'Complete blog production pipeline from research to publication',
    useCases: ['Content marketing', 'SEO optimization', 'Blog automation'],
    examples: ['...'],
  },
})

Discovering Services

Find services to compose:

// Search for services
const services = await db.Service.query({
  where: {
    category: 'Content Marketing',
    type: { in: ['ContentGeneration', 'SEO'] },
    rating: { gte: 4.5 },
  },
  orderBy: { popularity: 'desc' },
})

// Find complementary services
const complementary = await ai.recommend({
  model: 'gpt-5',
  for: myService,
  type: 'complementary-services',
  criteria: ['enhances-value', 'compatible-io', 'popular'],
})

// Auto-compose recommendation
const suggested = await ai.composeServices({
  goal: 'Create complete content marketing solution',
  available: services,
  constraints: {
    maxCost: 500,
    maxDuration: '10 minutes',
  },
})

Best Practices

1. Design for Composability

// Good: Clear inputs/outputs
const service = {
  inputs: {
    content: 'string',
    keywords: 'string[]',
  },
  outputs: {
    optimized: 'string',
    score: 'number',
  },
}

// Good: Minimal dependencies
const service = {
  dependencies: {
    required: [], // Self-contained
    optional: [$.Service['Enhancement']],
  },
}

2. Handle Partial Failures

// Save partial results
on.ServiceRequest.fail(async (request) => {
  if (request.partialResults) {
    db.PartialResult.create({
      requestId: request.id,
      results: request.partialResults,
      failedAt: request.failedStage,
    })

    // Offer resume option
    send.Customer.notify({
      customerId: request.customerId,
      message: 'Service partially completed. Resume?',
      action: { resume: request.id },
    })
  }
})

3. Optimize for Performance

// Cache intermediate results
const cache = new Map()

async function executeWithCache(service, inputs) {
  const cacheKey = `${service.id}:${hash(inputs)}`

  if (cache.has(cacheKey)) {
    return cache.get(cacheKey)
  }

  const result = await executeService(service, inputs)
  cache.set(cacheKey, result)

  return result
}

// Batch similar requests
const batcher = {
  queue: [],
  timer: null,

  add(request) {
    this.queue.push(request)

    if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), 100)
    }
  },

  async flush() {
    const batch = this.queue.splice(0)
    this.timer = null

    // Execute as batch
    const results = await executeBatch(batch)

    // Distribute results
    batch.forEach((req, i) => {
      deliverResult(req.id, results[i])
    })
  },
}

Next Steps