Lesson 10-Bun.js Microservices Architecture

Microservices Core Architecture Design

Microservices Core Components

Typical Microservices Architecture Diagram:

┌─────────────────────────────────────────────────┐
│                 API Gateway                    │
│  (Bun.js-based aggregation layer, routing, load balancing) │
└───────────────┬─────────────────┬───────────────┘
                │                 │
┌───────────────▼─────┐ ┌─────────▼───────────────┐
│   User Service       │ │    Order Service        │
│ (Independent Bun.js process) │ (Independent Bun.js process) │
└───────────────┬─────┘ └─────────┬───────────────┘
                │                 │
┌───────────────▼─────────────────▼───────────────┐
│               Database Cluster/Message Queue     │
│ (MySQL/PostgreSQL + Redis + RabbitMQ/Kafka)     │
└─────────────────────────────────────────────────┘

Service Splitting Principles:

  1. Business Capability-Oriented: Divide by business domains (user, order, payment)
  2. Data Independence: Each service owns its independent database
  3. Independent Deployment: Services communicate via lightweight protocols
  4. Fault Tolerance Boundaries: Failure of a single service does not affect the entire system

Communication Mechanism Design

gRPC Communication Implementation:

// proto/user.proto
syntax = "proto3";

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
}

message UserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
}

// src/services/userServiceGrpc.ts
import { serve } from 'bun'
import { loadProto } from '@grpc/proto-loader'
import { UserServiceClient } from './generated/user_grpc_web_pb'

// Generate gRPC code (requires protobuf tools)
// protoc --js_out=import_style=commonjs,binary:. \
//        --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. \
//        user.proto

const server = serve({
  port: 50051,
  grpc: {
    services: {
      UserService: {
        GetUser: async (call) => {
          const userId = call.request.getId()
          // Fetch user from database
          const user = await getUserFromDB(userId)
          return { id: user.id, name: user.name, email: user.email }
        },
        CreateUser: async (call) => {
          const { name, email } = call.request.toObject()
          // Create user logic
          const newUser = await createUserInDB({ name, email })
          return { id: newUser.id, name: newUser.name, email: newUser.email }
        }
      }
    }
  }
})

console.log('gRPC User Service running on port 50051')

RESTful Communication Implementation:

// src/services/orderService.ts
import { serve } from 'bun'

const orderService = serve({
  port: 3002,
  fetch(request) {
    const url = new URL(request.url)

    if (url.pathname === '/orders' && request.method === 'GET') {
      return getOrders(request)
    }

    if (url.pathname === '/orders' && request.method === 'POST') {
      return createOrder(request)
    }

    return new Response('Not Found', { status: 404 })
  }
})

async function getOrders(request: Request) {
  // JWT validation logic...
  const orders = await fetchOrdersFromDB()
  return new Response(JSON.stringify(orders), {
    headers: { 'Content-Type': 'application/json' }
  })
}

async function createOrder(request: Request) {
  const orderData = await request.json()
  // Business validation logic...
  const newOrder = await createOrderInDB(orderData)
  return new Response(JSON.stringify(newOrder), {
    status: 201,
    headers: { 'Content-Type': 'application/json' }
  })
}

console.log('Order Service running on http://localhost:3002')

Service Governance Implementation

Service Registration and Discovery

Consul-Based Service Registration:

// src/services/serviceRegistry.ts
import { serve } from 'bun'
import axios from 'axios'

const SERVICE_NAME = 'user-service'
const CONSUL_URL = 'http://localhost:8500'

// Register service on startup
async function registerService() {
  const serviceInfo = {
    Name: SERVICE_NAME,
    ID: `${SERVICE_NAME}-${process.pid}`,
    Address: 'localhost',
    Port: 3001,
    Check: {
      HTTP: `http://localhost:3001/health`,
      Interval: '10s',
      Timeout: '5s'
    }
  }

  try {
    await axios.put(`${CONSUL_URL}/v1/agent/service/register`, serviceInfo)
    console.log(`Service ${SERVICE_NAME} registered with Consul`)
  } catch (err) {
    console.error('Service registration failed:', err)
  }
}

// Service health check endpoint
serve({
  port: 3001,
  fetch(request) {
    if (request.url.endsWith('/health')) {
      return new Response('OK', { status: 200 })
    }
    // ...other route handling
  }
})

// Register service on startup
registerService()

// Deregister service on graceful shutdown
process.on('SIGTERM', async () => {
  await axios.put(`${CONSUL_URL}/v1/agent/service/deregister/${SERVICE_NAME}-${process.pid}`)
  process.exit(0)
})

Client-Side Service Discovery:

// src/utils/serviceDiscovery.ts
import axios from 'axios'

const CONSUL_URL = 'http://localhost:8500'

export async function discoverService(serviceName: string) {
  try {
    const response = await axios.get(`${CONSUL_URL}/v1/catalog/service/${serviceName}`)
    const instances = response.data
    if (instances.length === 0) {
      throw new Error(`No instances available for ${serviceName}`)
    }
    // Simple load balancing: random selection
    const instance = instances[Math.floor(Math.random() * instances.length)]
    return `http://${instance.ServiceAddress}:${instance.ServicePort}`
  } catch (err) {
    console.error('Service discovery failed:', err)
    throw err
  }
}

// Usage example
const userServiceUrl = await discoverService('user-service')
const response = await fetch(`${userServiceUrl}/users/123`)

Load Balancing Strategies

Client-Side Load Balancing Implementation:

// src/loadBalancer/roundRobin.ts
type ServiceInstance = {
  url: string
  healthy: boolean
}

export class RoundRobinLoadBalancer {
  private instances: ServiceInstance[]
  private currentIndex = 0

  constructor(instances: ServiceInstance[]) {
    this.instances = instances
  }

  getNextInstance(): ServiceInstance {
    let attempts = 0
    while (attempts < this.instances.length) {
      const instance = this.instances[this.currentIndex]
      this.currentIndex = (this.currentIndex + 1) % this.instances.length

      if (instance.healthy) {
        return instance
      }
      attempts++
    }
    throw new Error('No healthy instances available')
  }

  async healthCheck() {
    // Periodically check instance health
    for (const instance of this.instances) {
      try {
        await fetch(`${instance.url}/health`)
        instance.healthy = true
      } catch {
        instance.healthy = false
      }
    }
  }
}

// Usage example
const instances = [
  { url: 'http://localhost:3001', healthy: true },
  { url: 'http://localhost:3002', healthy: true }
]
const loadBalancer = new RoundRobinLoadBalancer(instances)

async function callUserService() {
  const instance = loadBalancer.getNextInstance()
  const response = await fetch(`${instance.url}/users/123`)
  return response.json()
}

Circuit Breaking and Fault Tolerance

Circuit Breaker Pattern Implementation:

// src/circuitBreaker/circuitBreaker.ts
export class CircuitBreaker {
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED'
  private failureCount = 0
  private lastFailureTime = 0
  private readonly resetTimeout: number
  private readonly failureThreshold: number

  constructor(
    private readonly action: () => Promise<any>,
    resetTimeout = 5000,
    failureThreshold = 3
  ) {
    this.resetTimeout = resetTimeout
    this.failureThreshold = failureThreshold
  }

  async execute(): Promise<any> {
    if (this.state === 'OPEN') {
      const now = Date.now()
      if (now - this.lastFailureTime > this.resetTimeout) {
        this.state = 'HALF_OPEN'
      } else {
        throw new Error('Circuit breaker is OPEN')
      }
    }

    try {
      const result = await this.action()
      if (this.state === 'HALF_OPEN') {
        this.state = 'CLOSED'
        this.failureCount = 0
      }
      return result
    } catch (err) {
      this.failureCount++
      this.lastFailureTime = Date.now()

      if (this.failureCount >= this.failureThreshold) {
        this.state = 'OPEN'
      }

      throw err
    }
  }
}

// Usage example
const breaker = new CircuitBreaker(async () => {
  const response = await fetch('http://user-service/users/123')
  if (!response.ok) throw new Error('Failed to fetch user')
  return response.json()
}, 5000, 3)

try {
  const user = await breaker.execute()
  console.log(user)
} catch (err) {
  console.error('Request failed:', err)
  // Return fallback response
  return { id: 'fallback', name: 'Default User' }
}

Data Consistency Assurance

Distributed Transaction Patterns

Saga Pattern Implementation:

// src/saga/orchestrator.ts
export class OrderSagaOrchestrator {
  async execute(orderData: any) {
    try {
      // Step 1: Create order (local transaction)
      const order = await createOrder(orderData)

      // Step 2: Reserve inventory (call inventory service)
      await this.callInventoryService(order.items)

      // Step 3: Deduct account balance (call payment service)
      await this.callPaymentService(order.userId, order.totalAmount)

      // All steps successful
      await confirmOrder(order.id)
      return { success: true }
    } catch (err) {
      // Execute compensating transaction
      await this.compensate(orderData)
      return { success: false, error: err }
    }
  }

  private async callInventoryService(items: any[]) {
    try {
      const response = await fetch('http://inventory-service/reserve', {
        method: 'POST',
        body: JSON.stringify(items)
      })
      if (!response.ok) throw new Error('Inventory reservation failed')
    } catch (err) {
      throw new Error(`Inventory service error: ${err.message}`)
    }
  }

  private async callPaymentService(userId: string, amount: number) {
    try {
      const response = await fetch('http://payment-service/charge', {
        method: 'POST',
        body: JSON.stringify({ userId, amount })
      })
      if (!response.ok) throw new Error('Payment failed')
    } catch (err) {
      throw new Error(`Payment service error: ${err.message}`)
    }
  }

  private async compensate(orderData: any) {
    // Compensation logic: cancel order, release inventory, etc.
    await cancelOrder(orderData.id)
    await releaseInventory(orderData.items)
  }
}

Event-Driven Architecture

Event Publish/Subscribe Implementation:

// src/eventBus/bunEventBus.ts
type EventHandler = (data: any) => Promise<void>

export class BunEventBus {
  private handlers: Map<string, EventHandler[]> = new Map()

  subscribe(eventType: string, handler: EventHandler) {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, [])
    }
    this.handlers.get(eventType)?.push(handler)
  }

  async publish(eventType: string, data: any) {
    const handlers = this.handlers.get(eventType)
    if (handlers) {
      await Promise.all(handlers.map(handler => handler(data)))
    }
  }
}

// Usage example
const eventBus = new BunEventBus()

// Subscribe to user created event
eventBus.subscribe('user.created', async (userData) => {
  console.log('Handling user created event:', userData)
  // Send welcome email, initialize user preferences, etc.
})

// Publish event in user service
serve({
  port: 3001,
  fetch(request) {
    if (request.url.endsWith('/users') && request.method === 'POST') {
      const userData = await request.json()
      // Save user to database...

      // Publish event
      eventBus.publish('user.created', userData)

      return new Response(JSON.stringify({ id: '123' }), {
        headers: { 'Content-Type': 'application/json' }
      })
    }
  }
})

Message Queue Integration (RabbitMQ):

// src/messageQueue/rabbitMq.ts
import { connect, Channel, Connection } from 'amqplib'

export class RabbitMQClient {
  private connection: Connection
  private channel: Channel

  async connect() {
    this.connection = await connect('amqp://localhost')
    this.channel = await this.connection.createChannel()
  }

  async publish(exchange: string, routingKey: string, message: any) {
    await this.channel.assertExchange(exchange, 'topic', { durable: true })
    this.channel.publish(
      exchange,
      routingKey,
      Buffer.from(JSON.stringify(message)),
      { persistent: true }
    )
  }

  async consume(queue: string, handler: (msg: any) => Promise<void>) {
    await this.channel.assertQueue(queue, { durable: true })
    await this.channel.consume(queue, async (msg) => {
      if (msg) {
        try {
          const message = JSON.parse(msg.content.toString())
          await handler(message)
          this.channel.ack(msg)
        } catch (err) {
          console.error('Message processing failed:', err)
          this.channel.nack(msg, false, true) // Retry
        }
      }
    })
  }
}

// Usage example
const rabbitMQ = new RabbitMQClient()
await rabbitMQ.connect()

// Order service publishes order created event
await rabbitMQ.publish('order-events', 'order.created', {
  orderId: '123',
  userId: '456',
  amount: 100
})

// Inventory service consumes event
await rabbitMQ.consume('inventory-queue', async (message) => {
  console.log('Received order created event:', message)
  // Deduct inventory logic...
})

Observability and Monitoring

Metrics Monitoring System

Prometheus Metrics Exposure:

// src/metrics/prometheus.ts
import { serve } from 'bun'
import { Registry, Counter, Gauge, Histogram } from 'prom-client'

const registry = new Registry()
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics
collectDefaultMetrics({ registry })

// Custom metrics
const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'path', 'status'],
  registry
})

const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'path'],
  buckets: [0.1, 0.5, 1, 2, 5],
  registry
})

// Middleware example
export function metricsMiddleware(ctx: any, next: () => Promise<any>) {
  const start = Date.now()
  const path = ctx.request.url
  const method = ctx.request.method

  return next().then(() => {
    const duration = (Date.now() - start) / 1000
    const status = ctx.response.status

    httpRequestsTotal.inc({ method, path, status })
    httpRequestDuration.observe({ method, path }, duration)
  })
}

// Metrics endpoint
serve({
  port: 9090,
  fetch() {
    return new Response(registry.metrics(), {
      headers: { 'Content-Type': registry.contentType }
    })
  }
})

console.log('Prometheus metrics server running on port 9090')

Distributed Tracing

OpenTelemetry Integration:

// src/tracing/opentelemetry.ts
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'

// Create Tracer Provider
const provider = new NodeTracerProvider()
provider.register()

// Configure Jaeger exporter
const jaegerExporter = new JaegerExporter({
  endpoint: 'http://localhost:14268/api/traces',
  serviceName: 'bun-microservice'
})

provider.addSpanProcessor(new SimpleSpanProcessor(jaegerExporter))

// Automatically instrument HTTP
registerInstrumentations({
  instrumentations: [
    new HttpInstrumentation({
      requestHook: (span, request) => {
        span.setAttribute('http.url', request.url)
        span.setAttribute('http.method', request.method)
      }
    })
  ]
})

// Get Tracer instance
export const tracer = provider.getTracer('bun-microservice')

// Usage example
export async function createUser(ctx: any) {
  const span = tracer.startSpan('createUser')
  try {
    // Business logic...
    span.setAttribute('user.id', '123')
    span.setStatus({ code: 0 }) // OK
    return { id: '123' }
  } catch (err) {
    span.recordException(err)
    span.setStatus({ code: 2, message: err.message })
    throw err
  } finally {
    span.end()
  }
}

Deployment and Operations

Containerized Deployment

Docker Compose Orchestration:

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    environment:
      - CONSUL_HOST=consul
    depends_on:
      - consul

  user-service:
    build: ./user-service
    environment:
      - CONSUL_HOST=consul
    depends_on:
      - consul
      - mysql

  order-service:
    build: ./order-service
    environment:
      - CONSUL_HOST=consul
    depends_on:
      - consul
      - mysql
      - rabbitmq

  inventory-service:
    build: ./inventory-service
    environment:
      - CONSUL_HOST=consul
    depends_on:
      - consul
      - mysql

  consul:
    image: consul:latest
    ports:
      - "8500:8500"
    command: agent -dev -client=0.0.0.0

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: microservices
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"

  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  jaeger:
    image: jaegertracing/all-in-one
    ports:
      - "16686:16686"

volumes:
  mysql-data:

Bun Service Dockerfile:

# Multi-stage build optimization
FROM node:18-alpine as builder

WORKDIR /app
COPY . .
RUN bun install --production
RUN bun build

# Production image
FROM node:18-alpine

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
RUN bun install --production

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["bun", "run", "--bun", "dist/app.js"]

CI/CD Pipeline

GitHub Actions Configuration:

# .github/workflows/deploy.yml
name: Deploy Microservices

on:
  push:
    branches: [main]

env:
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: my-org/bun-microservice

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: ['api-gateway', 'user-service', 'order-service']

    steps:
      - uses: actions/checkout@v3

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.DOCKER_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push ${{ matrix.service }}
        run: |
          docker build -t ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}:${{ github.sha }} \
            -f ./${{ matrix.service }}/Dockerfile ./${{ matrix.service }}
          docker push ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}:${{ github.sha }}

      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/${{ matrix.service }} \
            ${{ matrix.service }}=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.service }}:${{ github.sha }}
        env:
          KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}

  smoke-test:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run integration tests
        run: |
          npm install -g newman
          newman run ./tests/api-tests.json \
            --environment=./tests/envs/production.json

Summary

Core advantages of Bun.js microservices architecture:

  1. Blazing Fast Startup: Bun’s cold start is 10-100x faster than Node.js, ideal for Serverless and frequent scaling scenarios
  2. Native Performance: Bun’s optimized JavaScript/TypeScript execution engine delivers higher throughput
  3. Unified Toolchain: Built-in testing, bundling, and HTTP server reduce dependency complexity
  4. Modern Protocol Support: Native support for gRPC, HTTP/2, and other modern communication protocols

Implementation Recommendations:

  1. Use Consul/Eureka for service discovery
  2. Leverage gRPC for high-performance inter-service communication
  3. Decouple services with event buses (RabbitMQ/Kafka)
  4. Manage distributed transactions with the Saga pattern
  5. Implement full-stack tracing with OpenTelemetry
  6. Use containerized deployment with Kubernetes orchestration

A microservices architecture built with Bun.js maximizes its performance advantages while maintaining system resilience and maintainability.

Share your love