Best Practices
Guidelines for designing and operating Services-as-Software
Follow these guidelines to build robust, scalable, and successful Services-as-Software.
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: 250msDocumentation
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 fixMonitor 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:
- Single Responsibility - One service, one purpose
- Autonomous Operation - Run without human intervention
- Error Handling - Graceful degradation and recovery
- Performance - Cache, batch, and optimize
- Security - Validate, authenticate, encrypt
- Observability - Log, trace, and monitor
- Testing - Unit, integration, and load tests
- Documentation - Clear API docs and README
- Deployment - Health checks and zero downtime
- User Experience - Fast, clear, helpful
Next Steps
- Architecture - Service architecture patterns
- Building Services - Build your service
- Examples - Learn from examples