Serverless Computing Principles

Module 26: Advanced Backend & API Development

Introduction to Serverless Computing

Serverless computing represents a cloud computing execution model where cloud providers dynamically manage the allocation and provisioning of servers. A serverless application runs in stateless compute containers that are event-triggered, ephemeral, and fully managed by the cloud provider.

Analogy: Taxi vs. Uber

Traditional cloud infrastructure is like owning or leasing a taxi:

  • You pay for the vehicle whether it's in use or idle
  • You're responsible for maintenance, insurance, and parking
  • You need to predict capacity (vehicle size) in advance

Serverless is like using Uber:

  • You only pay when you take a ride (code executes)
  • No responsibility for vehicle maintenance or management
  • Can instantly scale up to bigger or more vehicles as needed
  • No costs when not in use
graph TD A[Serverless Computing] --> B[Function as a Service FaaS] A --> C[Backend as a Service BaaS] B --> B1[AWS Lambda] B --> B2[Azure Functions] B --> B3[Google Cloud Functions] B --> B4[Cloudflare Workers] C --> C1[Auth Services] C --> C2[Database Services] C --> C3[Storage Services] C --> C4[API Services]

Core Principles of Serverless Architecture

Several fundamental principles define serverless computing:

No Server Management

Developers are completely abstracted away from server management. There's no need to provision, scale, or maintain servers.

Evolution of Infrastructure Management

graph LR A[Physical Servers] --> B[Virtual Machines] B --> C[Containers] C --> D[Serverless] style A fill:#f8cecc,stroke:#b85450 style B fill:#d5e8d4,stroke:#82b366 style C fill:#dae8fc,stroke:#6c8ebf style D fill:#fff2cc,stroke:#d6b656 A ----> E[You Manage: Hardware, OS, Runtime, App] B ----> F[You Manage: OS, Runtime, App] C ----> G[You Manage: App, Some Runtime] D ----> H[You Manage: Only App Code]

Pay-per-Execution

Billing is based on actual compute resources consumed rather than pre-purchased units of capacity. When code isn't running, you're not paying.

Cost Comparison Example: API with 1M Requests/Month

Hosting Type Average Cost Considerations
Dedicated Server $40-$100/month Constant cost regardless of actual usage
Container-based $20-$60/month Requires proper auto-scaling configuration
Serverless $0-$20/month Scales to zero, costs depend on execution time and memory usage

Auto-scaling

Applications automatically scale based on incoming load without any configuration or management by the developer.

Auto-scaling Comparison Traditional Server Scaling Idle servers still incur cost Manual scaling Serverless Scaling No idle resources Automatic scaling Traffic

Event-Driven Execution

Code is executed in response to events or requests. Functions are "triggered" by specific events like HTTP requests, database changes, file uploads, or scheduled events.

graph TD A[Event Sources] --> B[Serverless Function] B --> C[Resources/Services] A --> A1[HTTP Request] A --> A2[Database Change] A --> A3[File Upload] A --> A4[Message Queue] A --> A5[IoT Event] A --> A6[Scheduled Task] B --> B1[Function Execution Environment] C --> C1[Databases] C --> C2[Storage] C --> C3[Other Services] C --> C4[External APIs]

Stateless Nature

The execution environment is ephemeral. Each function invocation may run on a completely new instance with no access to previous state from prior executions.

Stateless Architecture Example


// AWS Lambda function handling user authentication
exports.handler = async (event) => {
  console.log('Received event:', JSON.stringify(event, null, 2));
  
  // Don't rely on variables outside the handler function
  // They might be initialized between invocations, but there's no guarantee
  
  // Don't store state in the filesystem
  // Any files you create might not be available in subsequent invocations
  
  // Instead, use external services for state
  const userId = event.userId;
  
  // Get user data from database (external state)
  const user = await getUserFromDatabase(userId);
  
  // Authenticate user
  const isAuthenticated = verifyCredentials(user, event.credentials);
  
  // Store authentication result in external service if needed
  if (isAuthenticated) {
    await storeAuthToken(userId, generateToken());
  }
  
  return {
    statusCode: isAuthenticated ? 200 : 401,
    body: JSON.stringify({
      authenticated: isAuthenticated,
      message: isAuthenticated ? 'Authentication successful' : 'Authentication failed'
    })
  };
};
        

Microservice-Friendly

Serverless architectures naturally encourage small, single-purpose functions that align well with microservice principles.

Monolith vs. Microservices vs. Functions

graph TD subgraph "Monolithic Application" A[Single Deployment Unit] A --> A1[User Module] A --> A2[Product Module] A --> A3[Order Module] A --> A4[Payment Module] end subgraph "Microservices" B1[User Service] B2[Product Service] B3[Order Service] B4[Payment Service] end subgraph "Serverless Functions" C1[Get User] C2[Update User] C3[Get Product] C4[Search Products] C5[Create Order] C6[Process Payment] end

Types of Serverless Computing

Serverless computing encompasses two main categories:

Function as a Service (FaaS)

FaaS platforms allow developers to deploy individual functions that respond to events. Code runs only when needed and scales automatically.


// Example AWS Lambda function in Node.js
exports.handler = async (event) => {
  console.log('Event:', JSON.stringify(event, null, 2));
  
  const name = event.queryStringParameters?.name || 'World';
  
  const response = {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      message: `Hello, ${name}!`,
      timestamp: new Date().toISOString()
    })
  };
  
  return response;
};
      

Backend as a Service (BaaS)

BaaS provides pre-built backend services that developers can integrate into their applications without managing server-side logic.

BaaS Integration Example


// Using Firebase Authentication and Firestore
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
import { getFirestore, collection, addDoc } from 'firebase/firestore';

// Initialize Firebase
const firebaseConfig = {
  apiKey: "your-api-key",
  authDomain: "your-project.firebaseapp.com",
  projectId: "your-project",
  storageBucket: "your-project.appspot.com",
  messagingSenderId: "your-messaging-id",
  appId: "your-app-id"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);

// Authentication
async function loginUser(email, password) {
  try {
    const userCredential = await signInWithEmailAndPassword(auth, email, password);
    return userCredential.user;
  } catch (error) {
    console.error('Authentication error:', error);
    throw error;
  }
}

// Database operation
async function createOrder(userId, orderData) {
  try {
    const docRef = await addDoc(collection(db, "orders"), {
      userId: userId,
      items: orderData.items,
      total: orderData.total,
      timestamp: new Date()
    });
    console.log("Order created with ID: ", docRef.id);
    return docRef.id;
  } catch (error) {
    console.error("Error adding order: ", error);
    throw error;
  }
}
        

Benefits of Serverless Architecture

Serverless computing offers numerous advantages for developers and organizations:

Reduced Operational Complexity

Developers can focus on writing code rather than managing infrastructure, patching servers, or configuring networks.

Operational Tasks Comparison

Task Traditional Servers Serverless
Server provisioning Manual or IaC Automatic
Capacity planning Required Not needed
OS patching Your responsibility Provider managed
Security updates Your responsibility Provider managed
High availability Complex configuration Built-in
Load balancing Manual setup Automatic
Scaling Manual or automated with thresholds Automatic and precise

Cost Efficiency

The pay-per-execution model eliminates idle capacity costs and can significantly reduce operational expenses for variable workloads.

Cost Comparison: Traditional vs. Serverless Time Cost Traditional Server Traffic Pattern Serverless Cost Savings Wasted Capacity

Rapid Development and Deployment

Serverless enables quick iteration and reduces time to market by simplifying the deployment process and eliminating infrastructure management.

Increased Scalability

Automatic scaling handles traffic spikes effortlessly without any manual intervention or capacity planning.

Real-World Scaling Example: Black Friday Sale

An e-commerce company using serverless architecture for their Black Friday sale:

  • Normal traffic: 100 requests/second
  • Sale period peak: 5,000 requests/second
  • With traditional architecture: Would need to provision 50x capacity weeks in advance
  • With serverless: Functions automatically scale up within seconds of traffic increase
  • Cost efficiency: Only paying for the actual compute used during the peak, not for the entire month

Built-in High Availability

Most serverless platforms automatically provide redundancy and fault tolerance across multiple availability zones.

Challenges and Limitations

Despite its advantages, serverless architecture comes with several challenges:

Cold Start Latency

When a function hasn't been used recently, it may need to be initialized before execution, causing additional latency.

sequenceDiagram participant Client participant Function participant Container Note over Function,Container: Cold Start Client->>Function: Request Function->>Container: Initialize Container Note right of Container: 100ms - 2s delay Container->>Function: Container Ready Function->>Function: Execute Code Function->>Client: Response Note over Function,Container: Warm Start Client->>Function: Request Note right of Function: Container already running Function->>Function: Execute Code Function->>Client: Response

Cold Start Mitigation Techniques

  • Keep functions warm: Schedule periodic pings to prevent idle timeout
  • Optimize function size: Smaller dependencies and code lead to faster initialization
  • Use provisioned concurrency: Pre-warm function instances (AWS Lambda)
  • Optimize language choice: Languages with faster startup times (Node.js vs Java)
  • Separate latency-sensitive functions: Use different functions for critical vs. non-critical paths

Execution Duration Limits

Most FaaS platforms impose limits on how long a function can run, making them unsuitable for long-running processes.

Platform Execution Time Limit
AWS Lambda 15 minutes
Azure Functions 10 minutes (Consumption plan)
Google Cloud Functions 9 minutes
Cloudflare Workers 30 seconds

State Management Complexity

The stateless nature of serverless functions requires careful design for state persistence and sharing.

State Management Approaches


// Example of state management with Redis in AWS Lambda
const Redis = require('ioredis');

// Initialize Redis client
let redisClient;

exports.handler = async (event) => {
  // Reuse Redis connection if it exists
  if (!redisClient) {
    console.log('Creating new Redis connection');
    redisClient = new Redis({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      password: process.env.REDIS_PASSWORD
    });
  }
  
  const sessionId = event.headers.sessionId;
  
  // Step 1: Retrieve state from external store
  let sessionData;
  try {
    const sessionJson = await redisClient.get(`session:${sessionId}`);
    sessionData = sessionJson ? JSON.parse(sessionJson) : { visits: 0 };
  } catch (error) {
    console.error('Error retrieving session data:', error);
    sessionData = { visits: 0 };
  }
  
  // Step 2: Update state
  sessionData.visits += 1;
  sessionData.lastVisit = new Date().toISOString();
  
  // Step 3: Store updated state
  try {
    await redisClient.set(`session:${sessionId}`, JSON.stringify(sessionData), 'EX', 3600); // Expire in 1 hour
  } catch (error) {
    console.error('Error storing session data:', error);
  }
  
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: `Welcome back! Visit count: ${sessionData.visits}`,
      lastVisit: sessionData.visits > 1 ? sessionData.lastVisit : null
    })
  };
};
        

Testing and Debugging Challenges

Local testing of serverless functions can be challenging due to dependencies on cloud services and the execution environment.

Local Testing Tools

  • AWS SAM CLI: Local testing for AWS Lambda functions
  • Serverless Framework: Cross-platform local invocation
  • LocalStack: AWS cloud service emulator
  • Azure Functions Core Tools: Local Azure Functions runtime
  • Firebase Emulator Suite: Local Firebase services

Vendor Lock-in

Heavy reliance on provider-specific services can lead to vendor lock-in and make it difficult to switch providers.

Reducing Vendor Lock-in

  • Use abstraction layers: Tools like Serverless Framework provide cross-provider compatibility
  • Hexagonal architecture: Separate core business logic from provider-specific code
  • Dependency injection: Abstract cloud services behind interfaces
  • Open standards: Use CloudEvents for event standardization
  • Infrastructure as Code: Use IaC tools with multi-cloud support

Common Serverless Use Cases

Serverless architecture excels in various scenarios:

API Backends

Building RESTful or GraphQL APIs without managing server infrastructure.

graph LR A[Client] --> B[API Gateway] B --> C[Authentication Function] B --> D[User Function] B --> E[Product Function] B --> F[Order Function] C --> G[Auth Service] D --> H[(User Database)] E --> I[(Product Database)] F --> J[(Order Database)]

Serverless REST API Example with AWS SAM


# AWS SAM template for a serverless API
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  # API Gateway
  ProductsApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      Cors:
        AllowOrigin: "'*'"
        AllowMethods: "'GET,POST,PUT,DELETE'"
        AllowHeaders: "'Content-Type,Authorization'"

  # Lambda Functions
  GetProductsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src/
      Handler: getProducts.handler
      Runtime: nodejs16.x
      Events:
        GetProducts:
          Type: Api
          Properties:
            RestApiId: !Ref ProductsApi
            Path: /products
            Method: get

  GetProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src/
      Handler: getProduct.handler
      Runtime: nodejs16.x
      Events:
        GetProduct:
          Type: Api
          Properties:
            RestApiId: !Ref ProductsApi
            Path: /products/{id}
            Method: get

  CreateProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src/
      Handler: createProduct.handler
      Runtime: nodejs16.x
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductsTable
      Events:
        CreateProduct:
          Type: Api
          Properties:
            RestApiId: !Ref ProductsApi
            Path: /products
            Method: post

  # DynamoDB Table
  ProductsTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

Outputs:
  ApiEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ProductsApi}.execute-api.${AWS::Region}.amazonaws.com/prod/"
        

Data Processing

Processing uploads, transformations, and other operations on data as it moves through the system.

Image Processing Pipeline

graph TD A[User] -->|Uploads Image| B[Object Storage] B -->|Triggers| C[Resize Function] B -->|Triggers| D[Add Watermark Function] B -->|Triggers| E[Extract Metadata Function] C -->|Saves| F[Thumbnails Bucket] D -->|Saves| G[Watermarked Bucket] E -->|Stores| H[(Metadata Database)]

Webhooks and Integrations

Handling incoming webhooks from third-party services or implementing outgoing integrations.

Scheduled Tasks

Running periodic jobs without maintaining always-on server infrastructure.

Scheduled Task Example with AWS EventBridge


# CloudFormation template for a scheduled task
Resources:
  ScheduledFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./src/
      Handler: dailyReport.handler
      Runtime: nodejs16.x
      Events:
        DailyReport:
          Type: Schedule
          Properties:
            Schedule: cron(0 8 * * ? *)  # Run at 8 AM UTC every day
            Description: "Generate daily sales report"
            Enabled: true
      Environment:
        Variables:
          DATABASE_URL: !Ref DatabaseUrl
          REPORT_BUCKET: !Ref ReportBucket
          
  ReportBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${AWS::StackName}-reports"
        

Real-time Notifications

Sending messages and notifications to users based on events in the system.

Serverless Architecture Patterns

Several architectural patterns have emerged in serverless applications:

Function Composition

Breaking down complex workflows into chains of simple functions that work together.

graph LR A[Request] --> B[Validation Function] B --> C[Business Logic Function] C --> D[Database Function] D --> E[Response Formatting Function] E --> F[Response]

Function Composition with AWS Step Functions


{
  "Comment": "Order processing workflow",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:ValidateOrder",
      "Next": "CheckInventory",
      "Catch": [
        {
          "ErrorEquals": ["ValidationError"],
          "Next": "OrderRejected"
        }
      ]
    },
    "CheckInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:CheckInventory",
      "Next": "ProcessPayment",
      "Catch": [
        {
          "ErrorEquals": ["OutOfStockError"],
          "Next": "OrderRejected"
        }
      ]
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:ProcessPayment",
      "Next": "CreateOrder",
      "Catch": [
        {
          "ErrorEquals": ["PaymentFailedError"],
          "Next": "OrderRejected"
        }
      ]
    },
    "CreateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:CreateOrder",
      "Next": "NotifyCustomer"
    },
    "NotifyCustomer": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:NotifyCustomer",
      "End": true
    },
    "OrderRejected": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789012:function:HandleRejection",
      "End": true
    }
  }
}
        

Event-Driven Architecture

Using events as the primary mechanism for communication between decoupled services.

graph TD A[User] -->|Creates| B[Order] B -->|Publishes| C[Order Created Event] C -->|Triggers| D[Inventory Service] C -->|Triggers| E[Notification Service] C -->|Triggers| F[Analytics Service] D -->|Publishes| G[Inventory Updated Event] G -->|Triggers| H[Shipping Service]

Backend for Frontend (BFF)

Creating specialized backends for different frontend clients using serverless functions.

graph TD A[Mobile App] --> B[Mobile BFF Functions] C[Web App] --> D[Web BFF Functions] E[3rd Party API] --> F[API BFF Functions] B --> G[Core Services] D --> G F --> G

Distributed Transactions

Managing transactions across multiple functions and services using patterns like Sagas.

Saga Pattern Implementation

stateDiagram-v2 [*] --> ValidateOrder ValidateOrder --> CheckInventory: Valid ValidateOrder --> RejectOrder: Invalid CheckInventory --> ProcessPayment: In Stock CheckInventory --> RestoreInventory: Out of Stock RestoreInventory --> RejectOrder ProcessPayment --> CreateShipment: Payment Successful ProcessPayment --> RefundPayment: Payment Failed RefundPayment --> RejectOrder CreateShipment --> NotifyCustomer: Success CreateShipment --> CancelShipment: Failed CancelShipment --> RefundPayment NotifyCustomer --> [*]: Complete RejectOrder --> [*]: Failed

Practical Exercise

Designing a Serverless E-commerce System

In this exercise, you'll design a serverless architecture for a basic e-commerce system with the following requirements:

For each component, identify:

  1. What serverless services you would use (FaaS, BaaS)
  2. The triggering events for each function
  3. Data storage strategy
  4. How components communicate with each other

Create a high-level architecture diagram showing the components and their interactions.

Starting Point: User Authentication Flow

sequenceDiagram participant Client participant AuthFunction participant IdentityService participant UserDB Client->>AuthFunction: Login Request AuthFunction->>IdentityService: Validate Credentials IdentityService-->>AuthFunction: Auth Result alt Authentication Successful AuthFunction->>UserDB: Get User Profile UserDB-->>AuthFunction: User Data AuthFunction-->>Client: JWT Token + User Profile else Authentication Failed AuthFunction-->>Client: Auth Error end

Conclusion and Key Takeaways

In the next lecture, we'll explore AWS Lambda in depth, examining how to develop, deploy, and optimize serverless functions in AWS.

Additional Resources