.do
ScaleServices-as-Software

Best Practices

Guidelines for designing and operating Services-as-Software

Follow these guidelines to build robust, scalable, and successful Services-as-Software.

graph TB BP[Best Practices] BP --> Design[Service Design] BP --> Errors[Error Handling] BP --> Perf[Performance] BP --> Sec[Security] BP --> Obs[Observability] BP --> Test[Testing] BP --> Doc[Documentation] BP --> Deploy[Deployment] BP --> Price[Pricing] BP --> UX[User Experience] Design --> D1[Single Responsibility] Design --> D2[Autonomous Operation] Design --> D3[Clear Interfaces] Errors --> E1[Graceful Degradation] Errors --> E2[Retry with Backoff] Errors --> E3[Error Context] Perf --> P1[Caching] Perf --> P2[Batch Operations] Perf --> P3[Lazy Loading] Sec --> S1[Input Validation] Sec --> S2[Rate Limiting] Sec --> S3[Authentication] Sec --> S4[Encryption]

Service Design

Single Responsibility

Each service should have one clear purpose:

// Good: Focused service
export default service({
  name: 'Email Verification Service',
  description: 'Verify and validate email addresses',
  // Single, clear responsibility
})

// Bad: Service doing too much
export default service({
  name: 'Everything Service',
  description: 'Email, SMS, payments, analytics, and more',
  // Too many responsibilities
})

Autonomous Operation

Services should run without human intervention:

// Good: Fully autonomous
on: {
  '$.Order.created': async (event) => {
    // Validate automatically
    const valid = await validateOrder(event.data)

    if (valid) {
      // Process automatically
      await processOrder(event.data)
    } else {
      // Handle errors automatically
      await handleInvalidOrder(event.data)
    }
  },
}

// Bad: Requires manual intervention
on: {
  '$.Order.created': async (event) => {
    // Create task for human review
    await $.Task.create({
      type: 'manual-review',
      data: event.data,
    })
  },
}

Clear Interfaces

Define clear, semantic interfaces:

// Good: Clear, semantic API
api: {
  'POST /analyze': async (req) => {
    const { text, options } = await req.json()
    return await analyzeText(text, options)
  },

  'GET /analysis/:id': async (req) => {
    const { id } = req.params
    return await getAnalysis(id)
  },
}

// Bad: Unclear, inconsistent API
api: {
  'POST /do-stuff': async (req) => {
    // Unclear what this does
  },

  'GET /thing': async (req) => {
    // What thing?
  },
}

Error Handling

Graceful Degradation

Handle failures gracefully:

on: {
  '$.Content.generate': async (event) => {
    try {
      // Try premium AI model
      return await ai.generate({
        model: 'gpt-5',
        prompt: event.data.prompt,
      })
    } catch (error) {
      if (error.code === 'MODEL_UNAVAILABLE') {
        // Fall back to alternative model
        return await ai.generate({
          model: 'claude-sonnet-4.5',
          prompt: event.data.prompt,
        })
      }
      throw error
    }
  },
}

Retry with Backoff

Retry transient failures:

async function retryWithBackoff(fn, options = {}) {
  const { attempts = 3, delay = 1000, backoff = 2, retryableErrors = ['RATE_LIMIT', 'TIMEOUT', 'UNAVAILABLE'] } = options

  for (let attempt = 1; attempt <= attempts; attempt++) {
    try {
      return await fn()
    } catch (error) {
      const isLastAttempt = attempt === attempts
      const isRetryable = retryableErrors.includes(error.code)

      if (isLastAttempt || !isRetryable) {
        throw error
      }

      const waitTime = delay * Math.pow(backoff, attempt - 1)
      await new Promise((resolve) => setTimeout(resolve, waitTime))
    }
  }
}

Error Context

Provide useful error context:

try {
  await processPayment(payment)
} catch (error) {
  throw new Error('Payment processing failed', {
    cause: error,
    context: {
      paymentId: payment.id,
      customerId: payment.customerId,
      amount: payment.amount,
      timestamp: new Date(),
    },
  })
}

Performance

Caching

Cache frequently accessed data:

async function getCustomer(customerId) {
  const cacheKey = `customer:${customerId}`

  // Check cache
  let customer = await $.cache.get(cacheKey)

  if (!customer) {
    // Load from database
    customer = await $.Customer.findById(customerId)

    // Cache for 5 minutes
    await $.cache.set(cacheKey, customer, { ttl: 300 })
  }

  return customer
}

Batch Operations

Batch operations when possible:

// Good: Batch processing
on: {
  '$.Daily.report': async (event) => {
    const customers = await $.Customer.findMany()

    // Process in batches of 100
    const batches = chunk(customers, 100)

    for (const batch of batches) {
      await Promise.all(
        batch.map((customer) => generateReport(customer))
      )
    }
  },
}

// Bad: Sequential processing
on: {
  '$.Daily.report': async (event) => {
    const customers = await $.Customer.findMany()

    for (const customer of customers) {
      await generateReport(customer)  // One at a time
    }
  },
}

Lazy Loading

Load data only when needed:

class Customer {
  constructor(data) {
    this.id = data.id
    this.email = data.email
    // Don't load orders yet
    this._orders = null
  }

  async orders() {
    // Load orders only when accessed
    if (!this._orders) {
      this._orders = await $.Order.findMany({
        where: { customerId: this.id },
      })
    }
    return this._orders
  }
}

Security

Input Validation

Always validate input:

api: {
  'POST /process': async (req) => {
    const data = await req.json()

    // Validate input
    const schema = {
      email: 'string',
      amount: 'number',
      items: 'array',
    }

    const errors = validate(data, schema)
    if (errors.length > 0) {
      return new Response(JSON.stringify({ errors }), {
        status: 400,
      })
    }

    // Process valid data
    return await process(data)
  },
}

Rate Limiting

Protect against abuse:

api: {
  'POST /api/generate': async (req) => {
    const apiKey = req.headers.get('x-api-key')

    // Check rate limit
    const limit = await $.RateLimit.check({
      key: `api:${apiKey}`,
      limit: 100,
      window: '1h',
    })

    if (limit.exceeded) {
      return new Response('Rate limit exceeded', {
        status: 429,
        headers: {
          'Retry-After': limit.resetIn.toString(),
        },
      })
    }

    return await processRequest(req)
  },
}

Authentication

Verify user identity:

api: {
  'POST /sensitive': async (req) => {
    // Verify JWT token
    const token = req.headers.get('authorization')?.replace('Bearer ', '')

    if (!token) {
      return new Response('Unauthorized', { status: 401 })
    }

    try {
      const user = await $.auth.verify(token)
      // Process authenticated request
      return await processRequest(req, user)
    } catch (error) {
      return new Response('Invalid token', { status: 401 })
    }
  },
}

Data Encryption

Encrypt sensitive data:

// Encrypt before storing
const encrypted = await $.crypto.encrypt(sensitiveData, {
  algorithm: 'AES-256-GCM',
  key: process.env.ENCRYPTION_KEY,
})

await $.Database.create('SecretData', {
  userId: user.id,
  data: encrypted,
})

// Decrypt when retrieving
const record = await $.SecretData.findById(id)
const decrypted = await $.crypto.decrypt(record.data, {
  algorithm: 'AES-256-GCM',
  key: process.env.ENCRYPTION_KEY,
})

Observability

Structured Logging

Use structured logs:

// Good: Structured logging
$.log.info('Payment processed', {
  paymentId: payment.id,
  customerId: payment.customerId,
  amount: payment.amount,
  currency: payment.currency,
  timestamp: new Date(),
})

// Bad: Unstructured logging
console.log(`Payment ${payment.id} processed for $${payment.amount}`)

Metrics

Track important metrics:

on: {
  '$.Request.processed': async (event) => {
    // Record success metric
    await $.metric.increment('requests.success', {
      endpoint: event.data.endpoint,
      method: event.data.method,
    })

    // Record latency
    await $.metric.histogram('requests.latency', event.data.duration, {
      endpoint: event.data.endpoint,
    })
  },
}

Distributed Tracing

Trace requests across services:

async function processOrder(order) {
  const trace = $.trace.start('process-order', {
    orderId: order.id,
  })

  try {
    await trace.span('validate', () => validateOrder(order))
    await trace.span('reserve-inventory', () => reserveInventory(order))
    await trace.span('charge-payment', () => chargePayment(order))
    await trace.span('create-shipment', () => createShipment(order))

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

Testing

Unit Tests

Test individual functions:

import { describe, it, expect } from '@dotdo/test'

describe('calculateTotal', () => {
  it('calculates total with tax', () => {
    const result = calculateTotal({
      subtotal: 100,
      taxRate: 0.08,
    })

    expect(result).toBe(108)
  })

  it('applies discount', () => {
    const result = calculateTotal({
      subtotal: 100,
      discount: 0.1,
      taxRate: 0.08,
    })

    expect(result).toBe(97.2)
  })
})

Integration Tests

Test service integration:

describe('Order Processing Service', () => {
  it('processes complete order flow', async () => {
    // Create test order
    const order = await createTestOrder()

    // Emit event
    await $.send('$.Order.created', order)

    // Wait for processing
    await wait(1000)

    // Verify results
    const updated = await $.Order.findById(order.id)
    expect(updated.status).toBe('$.Processed')
    expect(updated.payment.status).toBe('$.Paid')
    expect(updated.shipment).toBeDefined()
  })
})

Load Testing

Test under load:

$ do load-test --endpoint /api/process --rps 1000 --duration 60s

Running load test...
Requests per second: 1000
Duration: 60s

Results:
  Total requests: 60,000
  Successful: 59,987 (99.98%)
  Failed: 13 (0.02%)
  Average latency: 45ms
  P95 latency: 125ms
  P99 latency: 250ms

Documentation

API Documentation

Document your API:

api: {
  /**
   * Analyze text content
   *
   * @endpoint POST /analyze
   * @param {string} text - Text to analyze
   * @param {object} options - Analysis options
   * @param {string[]} options.features - Features to extract
   * @returns {object} Analysis results
   *
   * @example
   * POST /analyze
   * {
   *   "text": "Sample text",
   *   "options": {
   *     "features": ["sentiment", "entities"]
   *   }
   * }
   */
  'POST /analyze': async (req) => {
    const { text, options } = await req.json()
    return await analyzeText(text, options)
  },
}

README

Maintain comprehensive README:

# Email Verification Service

AI-powered email verification and validation.

## Features

- Real-time email verification
- Disposable email detection
- Role account detection
- Syntax validation
- DNS validation
- SMTP validation

## Quick Start

```typescript
const result = await services.emailVerification.verify({
  email: '[email protected]',
})
```

API Reference

Document your service API thoroughly.

Pricing

Document your pricing model clearly.


### Changelog

Maintain changelog:

```markdown
# Changelog

## [2.1.0] - 2025-10-24

### Added
- Batch email verification
- Custom validation rules
- Webhook notifications

### Fixed
- SMTP timeout issues
- False positives for role accounts

### Deprecated
- Legacy API endpoints (sunset 2026-01-01)

Deployment

Health Checks

Implement health checks:

api: {
  'GET /health': async () => {
    const checks = {
      database: await checkDatabase(),
      ai: await checkAI(),
      external: await checkExternalAPIs(),
    }

    const healthy = Object.values(checks).every(Boolean)

    return {
      status: healthy ? 'healthy' : 'degraded',
      checks,
      version: '2.1.0',
      uptime: process.uptime(),
    }
  },
}

Graceful Shutdown

Handle shutdown gracefully:

export default service({
  name: 'My Service',

  lifecycle: {
    beforeShutdown: async () => {
      // Finish current requests
      await finishPendingRequests()

      // Close connections
      await $.db.close()

      // Flush logs
      await $.log.flush()
    },
  },
})

Zero-Downtime Updates

Deploy without downtime:

deployment: {
  strategy: 'rolling',
  health: {
    path: '/health',
    interval: 30,
  },
  rollback: {
    automatic: true,
    errorThreshold: 0.05,
  },
}

Pricing

Transparent Pricing

Make pricing clear:

// Good: Clear, simple pricing
pricing: {
  model: 'subscription',
  plans: [
    { name: 'Basic', price: 29, features: ['Feature 1', 'Feature 2'] },
    { name: 'Pro', price: 99, features: ['All Basic', 'Feature 3'] },
  ],
}

// Bad: Confusing pricing
pricing: {
  model: 'complex',
  basePrice: 19,
  perUserPrice: 5,
  perFeaturePrice: { a: 10, b: 15, c: 20 },
  minimumSpend: 50,
  // Too complex
}

Fair Usage

Set reasonable limits:

pricing: {
  plans: [
    {
      name: 'Starter',
      price: 29,
      limits: {
        requestsPerDay: 1000,
        requestsPerMonth: 30000,
      },
    },
  ],
}

User Experience

Fast Response Times

Optimize for speed:

// Good: Async processing
api: {
  'POST /process': async (req) => {
    const data = await req.json()

    // Queue for processing
    await $.queue.add('processing', data)

    // Return immediately
    return {
      status: 'processing',
      id: data.id,
      statusUrl: `/status/${data.id}`,
    }
  },
}

// Bad: Synchronous processing
api: {
  'POST /process': async (req) => {
    const data = await req.json()

    // User waits for processing
    const result = await processData(data)  // Could take minutes

    return result
  },
}

Clear Error Messages

Provide helpful errors:

// Good: Helpful error
if (!data.email) {
  throw new Error('Email is required. Please provide a valid email address.')
}

// Bad: Unclear error
if (!data.email) {
  throw new Error('Invalid input')
}

Progress Updates

Show progress for long operations:

api: {
  'POST /batch-process': async (req) => {
    const { items } = await req.json()
    const jobId = generateId()

    // Process in background
    processItems(items, {
      onProgress: (completed, total) => {
        $.progress.update(jobId, {
          completed,
          total,
          percent: (completed / total) * 100,
        })
      },
    })

    return {
      jobId,
      progressUrl: `/progress/${jobId}`,
    }
  },

  'GET /progress/:id': async (req) => {
    const { id } = req.params
    return await $.progress.get(id)
  },
}

Maintenance

Regular Updates

Keep dependencies updated:

$ pnpm update
$ pnpm audit
$ pnpm audit fix

Monitor Performance

Track performance trends:

$ do metrics --service my-service --range 7d

Performance Metrics (Last 7 Days):
  Average latency: 45ms (+5ms from last week)
  P95 latency: 125ms (+15ms)
  Error rate: 0.02% (-0.01%)
  Throughput: 1,250 req/s (+150)

Review Logs

Regularly review logs:

$ do logs --level error --range 7d

Recent Errors:
  2025-10-24 14:30  Payment API timeout (3 occurrences)
  2025-10-23 09:15  Database connection failed (1 occurrence)

Summary

Key principles for successful Services-as-Software:

  1. Single Responsibility - One service, one purpose
  2. Autonomous Operation - Run without human intervention
  3. Error Handling - Graceful degradation and recovery
  4. Performance - Cache, batch, and optimize
  5. Security - Validate, authenticate, encrypt
  6. Observability - Log, trace, and monitor
  7. Testing - Unit, integration, and load tests
  8. Documentation - Clear API docs and README
  9. Deployment - Health checks and zero downtime
  10. User Experience - Fast, clear, helpful

Next Steps