Back to blog

Serverless & FaaS Architecture: Build Mà Không Cần Quản Lý Server

software-architectureserverlesscloudbackendsystem-design
Serverless & FaaS Architecture: Build Mà Không Cần Quản Lý Server

Mọi architecture pattern chúng ta đã đề cập đều giả định bạn đang quản lý server — dù là monolith trên một máy, microservices trên cluster, hay container được Kubernetes orchestrate. Bạn provision capacity, xử lý scaling, patch hệ điều hành, và trả tiền cho idle time.

Serverless architecture loại bỏ gánh nặng operation đó. Bạn viết function, deploy chúng, và cloud provider xử lý mọi thứ còn lại: provisioning, scaling, patching, và billing. Bạn chỉ trả tiền cho những gì sử dụng — per request, per millisecond execution.

Nhưng "serverless" không có nghĩa là "không có server." Nó có nghĩa là bạn không cần nghĩ về server. Server vẫn tồn tại — chúng chỉ là problem của người khác.

Trong bài viết này, chúng ta sẽ đề cập:

✅ Serverless thực sự là gì (FaaS + BaaS)
✅ FaaS hoạt động thế nào: AWS Lambda, Azure Functions, Google Cloud Functions, Vercel
✅ Cold start và ảnh hưởng đến performance
✅ Stateless function design
✅ Event trigger: HTTP, queue, schedule, storage
✅ Serverless database: Neon, PlanetScale, DynamoDB
✅ Composition pattern: chaining, fan-out/fan-in, orchestration
✅ Cost model: pay-per-invocation vs always-on
✅ Limitation: execution time, memory, vendor lock-in
✅ Serverless framework: SST, Serverless Framework, SAM
✅ Khi nào serverless phù hợp vs server truyền thống
✅ Hybrid architecture


Serverless Là Gì?

Serverless là thuật ngữ bao quát cho hai danh mục:

Function-as-a-Service (FaaS)

FaaS cho phép bạn deploy từng function riêng lẻ chạy để phản hồi event. Platform quản lý runtime, scaling, và infrastructure.

Ví dụ: AWS Lambda, Azure Functions, Google Cloud Functions, Vercel Serverless Functions, Cloudflare Workers.

Backend-as-a-Service (BaaS)

BaaS cung cấp backend service được quản lý hoàn toàn mà bạn consume qua API thay vì build từ đầu.

Ví dụ:

  • Authentication: Auth0, Clerk, Firebase Auth, Neon Auth
  • Database: Neon (Postgres), PlanetScale (MySQL), DynamoDB, Supabase
  • Storage: S3, Cloudflare R2, Supabase Storage
  • Real-time: Pusher, Ably, Supabase Realtime
  • Email: Resend, SendGrid, SES

Ứng dụng serverless thường kết hợp FaaS (cho custom logic) với BaaS (cho managed infrastructure).


FaaS Hoạt Động Thế Nào

Lifecycle Của Một Function Invocation

Ví Dụ AWS Lambda

// handler.ts — Một AWS Lambda function đơn giản
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
 
// Initialization code — chạy MỘT LẦN per cold start
const dbClient = new DatabaseClient(process.env.DATABASE_URL!);
 
// Handler — chạy MỖI invocation
export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const { httpMethod, path, body } = event;
 
  if (httpMethod === "GET" && path === "/api/users") {
    const users = await dbClient.query("SELECT * FROM users LIMIT 50");
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(users),
    };
  }
 
  if (httpMethod === "POST" && path === "/api/users") {
    const { name, email } = JSON.parse(body || "{}");
    const user = await dbClient.query(
      "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
      [name, email]
    );
    return {
      statusCode: 201,
      body: JSON.stringify(user),
    };
  }
 
  return { statusCode: 404, body: "Not Found" };
};

Vercel Serverless Functions (Next.js)

// app/api/users/route.ts — Next.js Route Handler (chạy như serverless)
import { NextRequest, NextResponse } from "next/server";
import { neon } from "@neondatabase/serverless";
 
const sql = neon(process.env.DATABASE_URL!);
 
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const limit = searchParams.get("limit") || "50";
 
  const users = await sql`SELECT * FROM users LIMIT ${limit}`;
 
  return NextResponse.json(users);
}
 
export async function POST(request: NextRequest) {
  const { name, email } = await request.json();
 
  const [user] = await sql`
    INSERT INTO users (name, email)
    VALUES (${name}, ${email})
    RETURNING *
  `;
 
  return NextResponse.json(user, { status: 201 });
}

Azure Functions

// src/functions/getUsers.ts
import { app, HttpRequest, HttpResponseInit } from "@azure/functions";
 
app.http("getUsers", {
  methods: ["GET"],
  authLevel: "anonymous",
  route: "users",
  handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
    const users = await db.query("SELECT * FROM users");
    return {
      status: 200,
      jsonBody: users,
    };
  },
});

Cold Start: Cái Giá Của Serverless

Cold start xảy ra khi platform cần tạo container mới cho function. Nó bao gồm:

  1. Provisioning container/micro-VM
  2. Download code package
  3. Initialize runtime (Node.js, Python, Java, v.v.)
  4. Chạy initialization code (database connection, config loading)

So Sánh Cold Start

RuntimeCold Start (điển hình)Ghi chú
Node.js100-500msNhanh, runtime nhỏ
Python100-500msNhanh, tương tự Node.js
Go50-200msCompiled binary, nhanh nhất
Rust50-200msCompiled binary, rất nhanh
Java1-10sJVM startup, class loading
.NET500ms-3sCLR initialization

Chiến Lược Giảm Cold Start

1. Giữ function nhỏ

// ❌ Một function khổng lồ với nhiều dependency
import express from "express";
import mongoose from "mongoose";
import redis from "redis";
import { S3Client } from "@aws-sdk/client-s3";
import sharp from "sharp"; // Image processing — nặng!
// 50MB deployment package, cold start chậm
 
// ✅ Function focused với dependency tối thiểu
import { neon } from "@neondatabase/serverless"; // ~200KB
// 2MB deployment package, cold start nhanh

2. Dùng provisioned concurrency (AWS Lambda)

# serverless.yml
functions:
  api:
    handler: handler.main
    provisionedConcurrency: 5  # 5 instance luôn warm

3. Initialize ngoài handler

// ✅ Database client tạo MỘT LẦN, reuse qua các invocation
const sql = neon(process.env.DATABASE_URL!);
 
export async function handler(event: any) {
  // sql đã initialized trên warm start
  const result = await sql`SELECT 1`;
  return { statusCode: 200, body: "OK" };
}

4. Chọn runtime nhanh cho function nhạy cảm latency

Go và Rust có cold start nhanh nhất. Với Node.js, dùng lightweight dependency và tree-shake code không dùng.


Stateless Function Design

Serverless function phải stateless — mỗi invocation là độc lập, không có shared memory giữa các lần gọi.

// ❌ SAI — Stateful: dựa vào in-memory state
let requestCount = 0;  // Reset khi cold start, không shared giữa instance
 
export async function handler(event: any) {
  requestCount++;  // Không đáng tin! Instance khác nhau có count khác nhau
  return { body: `Request #${requestCount}` };
}
// ✅ ĐÚNG — Stateless: dùng external storage
import { Redis } from "@upstash/redis";
 
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});
 
export async function handler(event: any) {
  const count = await redis.incr("request-count");
  return { body: `Request #${count}` };
}

Lưu state ở đâu:

Loại stateGiải phápVí dụ
Session dataRedis (Upstash), JWT tokenUser login session
Persistent dataDatabase (Neon, DynamoDB)User profile, order
CacheRedis (Upstash), CDNAPI response, computed value
File uploadObject storage (S3, R2)Image, document
Temporary stateStep Functions, workflow engineMulti-step process

Event Trigger

Serverless function phản hồi event — không chỉ HTTP request.

HTTP Trigger

Trigger phổ biến nhất — function hoạt động như API endpoint.

// Vercel: app/api/webhook/route.ts
export async function POST(request: NextRequest) {
  const payload = await request.json();
  await processWebhook(payload);
  return NextResponse.json({ received: true });
}

Queue Trigger

Process message từ queue một cách async.

// AWS Lambda: SQS trigger
export async function handler(event: SQSEvent) {
  for (const record of event.Records) {
    const message = JSON.parse(record.body);
    await processOrder(message);
  }
}

Schedule Trigger (Cron)

Chạy function theo lịch — hoàn hảo cho periodic task.

// Vercel: app/api/cron/daily-report/route.ts
export async function GET(request: NextRequest) {
  // Verify cron secret
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const report = await generateDailyReport();
  await sendReportEmail(report);
  return NextResponse.json({ success: true });
}
// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/daily-report",
      "schedule": "0 9 * * *"
    }
  ]
}

Storage Trigger

Phản ứng với file upload hoặc thay đổi trong object storage.

// AWS Lambda: S3 trigger — process uploaded image
export async function handler(event: S3Event) {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = record.s3.object.key;
 
    // Download, resize, và upload thumbnail
    const image = await s3.getObject({ Bucket: bucket, Key: key });
    const thumbnail = await sharp(image.Body).resize(200, 200).toBuffer();
    await s3.putObject({
      Bucket: bucket,
      Key: `thumbnails/${key}`,
      Body: thumbnail,
    });
  }
}

Database Trigger

Phản ứng với database change — có sẵn trên một số platform.

// Supabase: Database webhook khi có user mới
// Trigger khi row được insert vào bảng "users"
export async function handler(event: any) {
  const newUser = event.record;
  await sendWelcomeEmail(newUser.email);
  await createDefaultSettings(newUser.id);
}

Serverless Database

Database truyền thống giả định long-lived connection từ vài server. Serverless function tạo nhiều short-lived connection từ nhiều instance, có thể áp đảo database truyền thống.

Connection Problem

100 Lambda invocation = 100 database connection. Scale lên 1000 invocation và database hết connection.

Giải Pháp

1. Database serverless-native

Database được thiết kế cho serverless workload xử lý connection pooling tự động.

// Neon — serverless Postgres với HTTP driver
import { neon } from "@neondatabase/serverless";
 
const sql = neon(process.env.DATABASE_URL!);
 
export async function handler() {
  // HTTP-based query — không cần persistent connection
  const users = await sql`SELECT * FROM users WHERE active = true`;
  return { statusCode: 200, body: JSON.stringify(users) };
}

2. Connection pooling

Dùng connection pooler giữa function và database.

// Neon với connection pooling
// Connection string dùng pooler endpoint
const sql = neon(process.env.DATABASE_URL!);
// DATABASE_URL=postgres://user:[email protected]/neondb

3. DynamoDB — truly serverless NoSQL

import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
 
const client = new DynamoDBClient({});
 
export async function handler(event: any) {
  const result = await client.send(
    new GetItemCommand({
      TableName: "Users",
      Key: { userId: { S: event.pathParameters.id } },
    })
  );
  return { statusCode: 200, body: JSON.stringify(result.Item) };
}

So Sánh Serverless Database

DatabaseLoạiConnection ModelPhù hợp cho
NeonPostgreSQLHTTP + poolingFull SQL, branching, scale-to-zero
PlanetScaleMySQLHTTP driverMySQL workload, branching
SupabasePostgreSQLPooling + RESTFull-stack với auth, real-time
DynamoDBNoSQLHTTP (native)High-throughput key-value
Upstash RedisKey-valueHTTPCaching, rate limiting, queue
TursoSQLiteHTTPEdge-first, embedded

Composition Pattern

Function riêng lẻ thì đơn giản. Thách thức là compose chúng thành workflow phức tạp.

1. Function Chaining

Một function gọi function khác tuần tự.

// Simple chaining — mỗi function gọi function tiếp theo
export async function validateOrder(order: Order) {
  validateItems(order.items);
  validateAddress(order.shippingAddress);
 
  // Gọi function tiếp
  await processPayment(order);
}
 
async function processPayment(order: Order) {
  const payment = await stripe.charges.create({
    amount: order.total,
    currency: "usd",
  });
 
  await reserveInventory(order, payment.id);
}

Vấn đề: Nếu reserveInventory fail, bạn đã charge khách hàng. Không có error handling hay retry built-in.

2. Fan-Out / Fan-In

Phân phối công việc qua parallel function, rồi aggregate kết quả.

// Fan-out: Chia công việc thành parallel task
export async function processLargeDataset(event: any) {
  const records = await fetchRecords();
  const batches = chunk(records, 100); // Chia thành batch 100
 
  // Fan-out: invoke parallel Lambda function
  const promises = batches.map((batch, index) =>
    lambda.invoke({
      FunctionName: "processBatch",
      Payload: JSON.stringify({ batch, batchIndex: index }),
    })
  );
 
  const results = await Promise.all(promises);
 
  // Fan-in: aggregate kết quả
  const totalProcessed = results.reduce(
    (sum, r) => sum + JSON.parse(r.Payload!).processed,
    0
  );
  return { totalProcessed };
}

3. Orchestration (Step Functions / Durable Functions)

Cho workflow phức tạp, multi-step với error handling, retry, và human approval.

// AWS Step Functions state machine (đơn giản hóa)
{
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:validateOrder",
      "Next": "ProcessPayment",
      "Catch": [{ "ErrorEquals": ["ValidationError"], "Next": "OrderFailed" }]
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:processPayment",
      "Retry": [{ "ErrorEquals": ["TimeoutError"], "MaxAttempts": 3 }],
      "Next": "ReserveInventory",
      "Catch": [{ "ErrorEquals": ["PaymentDeclined"], "Next": "OrderFailed" }]
    },
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:reserveInventory",
      "Next": "SendConfirmation"
    },
    "SendConfirmation": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:sendConfirmation",
      "End": true
    },
    "OrderFailed": {
      "Type": "Fail",
      "Cause": "Order processing failed"
    }
  }
}

Khi Nào Dùng Mỗi Pattern

PatternDùng khiTránh khi
ChainingBước tuần tự đơn giản, all-or-nothingCần error handling phức tạp
Fan-out/fan-inCông việc parallel độc lậpCác bước depend vào nhau
OrchestrationWorkflow phức tạp, retry, human approval, long-runningSimple request-response
Event-drivenLoose coupling, eventual consistencyCần strict ordering

Cost Model

Pricing serverless khác biệt cơ bản so với hosting truyền thống.

Pay-Per-Invocation

Server truyền thống:
  1 EC2 t3.medium = $30/tháng (chạy 24/7, kể cả 2 giờ sáng với zero traffic)
 
Serverless (AWS Lambda):
  Giá = (invocation × $0.20/triệu) + (duration × $0.0000166667/GB-second)
 
  Ví dụ: 1 triệu request/tháng, trung bình 200ms, 256MB memory
  = (1M × $0.20/M) + (1M × 0.2s × 0.25GB × $0.0000166667)
  = $0.20 + $0.83
  = $1.03/tháng

Khi Nào Serverless Rẻ Hơn

Serverless tiết kiệm tiền khi:

  • Traffic spiky hoặc không dự đoán được
  • Nhiều endpoint với traffic riêng lẻ thấp
  • Development/staging environment (scale to zero)
  • Background job chạy định kỳ

Server rẻ hơn khi:

  • Traffic cao ổn định (>100M request/tháng)
  • Long-running process (>15 phút)
  • GPU workload hoặc computation nặng
  • Load dự đoán được, ổn định

Chi Phí Ẩn

  • Data transfer giữa các service (egress fee)
  • API Gateway charge ($3.50/triệu request trên AWS)
  • Step Functions state transition ($25/triệu transition)
  • CloudWatch log và monitoring
  • Provisioned concurrency để loại bỏ cold start

Limitation

Giới Hạn Execution Time

PlatformMax Execution Time
AWS Lambda15 phút
Azure Functions10 phút (Consumption), unlimited (Premium)
Google Cloud Functions9 phút (1st gen), 60 phút (2nd gen)
Vercel Serverless10 giây (Hobby), 300 giây (Pro)
Cloudflare Workers30 giây (Bundled), 15 phút (Unbound)

Giới Hạn Memory

PlatformMemory Range
AWS Lambda128MB - 10GB
Azure Functions256MB - 14GB
Vercel Serverless1024MB - 3008MB
Cloudflare Workers128MB

Giới Hạn Payload Size

PlatformRequest/Response Size
AWS Lambda6MB (sync), 256KB (async)
Azure Functions100MB
Vercel Serverless4.5MB request body
API Gateway10MB

Vendor Lock-In

Mỗi platform có API, deployment model, và configuration riêng. Chuyển từ AWS Lambda sang Azure Functions đòi hỏi viết lại infrastructure code.

Chiến lược giảm thiểu:

  • Giữ business logic trong pure function, tách biệt khỏi platform-specific handler
  • Dùng abstraction layer (ví dụ Serverless Framework)
  • Dùng standard protocol (HTTP, SQL) thay vì proprietary service khi có thể
  • Containerize function cho portability (AWS Lambda support container image)

Serverless Framework

Framework đơn giản hóa deployment, configuration, và local development.

SST (Serverless Stack)

Framework hiện đại build trên AWS CDK. Support Next.js, Remix, Astro, và hơn thế nữa.

// sst.config.ts
export default {
  config() {
    return { name: "my-app", region: "us-east-1" };
  },
  stacks(app) {
    app.stack(function API({ stack }) {
      const api = new Api(stack, "api", {
        routes: {
          "GET /users": "packages/functions/src/users.list",
          "POST /users": "packages/functions/src/users.create",
        },
      });
      stack.addOutputs({ ApiEndpoint: api.url });
    });
  },
};

Serverless Framework

Framework được dùng rộng rãi nhất. Support AWS, Azure, GCP.

# serverless.yml
service: my-api
provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
 
functions:
  getUsers:
    handler: src/handlers/users.getAll
    events:
      - http:
          path: /users
          method: get
 
  processOrder:
    handler: src/handlers/orders.process
    events:
      - sqs:
          arn: !GetAtt OrderQueue.Arn
          batchSize: 10
 
resources:
  Resources:
    OrderQueue:
      Type: AWS::SQS::Queue

AWS SAM (Serverless Application Model)

Framework native AWS dùng CloudFormation template.

# template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
 
Resources:
  GetUsersFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/users.getAll
      Runtime: nodejs20.x
      Events:
        GetUsers:
          Type: Api
          Properties:
            Path: /users
            Method: get

So Sánh Framework

FeatureSSTServerless FrameworkAWS SAM
Cloud supportAWSAWS, Azure, GCPChỉ AWS
Ngôn ngữTypeScriptYAML + pluginYAML (CloudFormation)
Local devLive Lambda (hot reload)Offline pluginLocal invoke
Full-stackNext.js, Remix, AstroAPI-focusedAPI-focused
Learning curveTrung bìnhThấpTrung bình

Khi Nào Serverless Phù Hợp

Rất Phù Hợp

1. API với traffic biến động

Sáng thứ Hai: 10,000 request/phút
Tối thứ Bảy: 50 request/phút
→ Serverless auto-scale và gần như không tốn tiền khi idle

2. Event processing pipeline

3. Scheduled task và cron job

Daily report generation → 1 Lambda invocation mỗi ngày
Weekly email digest → 1 invocation mỗi tuần
→ Trả tiền cho vài giây compute, không phải server 24/7

4. Webhook và integration

Function chờ callback từ bên thứ ba (Stripe, GitHub, Slack).

5. Prototype và MVP

Ship nhanh với zero infrastructure management. Scale sau nếu cần.

Không Phù Hợp

1. Long-running process

Video transcoding, ML model training, batch ETL job chạy hàng giờ.

2. WebSocket / persistent connection

FaaS tiêu chuẩn không support long-lived connection (dù Cloudflare Workers Durable Objects và AWS API Gateway WebSocket thì có).

3. Consistent high-throughput

Nếu bạn process 50,000 request/giây ổn định, container hoặc VM rẻ hơn đáng kể.

4. Local state requirement

In-memory cache, session state, hoặc data phải persist giữa các request.


Hybrid Architecture

Đa số production system dùng kết hợp serverless và component truyền thống.

Pattern phổ biến:

  • API layer: Serverless (auto-scaling, pay-per-use)
  • Real-time: Always-on WebSocket server
  • Background processing: Queue + serverless worker (cho task ngắn) hoặc container (cho task dài)
  • Database: Serverless-native (Neon, DynamoDB)
  • Caching: Serverless Redis (Upstash)

Tổng Kết

Serverless architecture chuyển trách nhiệm operation từ bạn sang cloud provider, cho phép bạn tập trung vào code thay vì infrastructure.

Concept cốt lõi:

  • FaaS — deploy function riêng lẻ tự động scale
  • BaaS — dùng managed service (auth, database, storage) thay vì tự build
  • Stateless design — function không có shared memory; dùng external state store
  • Event-driven — function phản hồi trigger (HTTP, queue, schedule, storage event)

Lợi ích chính:

  • Zero server management (không provisioning, patching, scaling decision)
  • Pay-per-use pricing (không tốn tiền khi idle)
  • Automatic scaling từ zero đến hàng ngàn concurrent invocation
  • Time-to-market nhanh hơn (focus vào business logic, không phải infrastructure)

Thách thức chính:

  • Cold start thêm latency (giảm bằng provisioned concurrency, runtime nhanh)
  • Execution time limit (tối đa 15 phút trên Lambda)
  • Vendor lock-in (giữ business logic portable)
  • Debugging và observability khó hơn (distributed tracing, structured logging)
  • Connection management với traditional database (dùng serverless database)

Khi nào nên dùng:

  • Traffic biến động hoặc không dự đoán được
  • Event processing, webhook, scheduled task
  • API nơi per-endpoint scaling hợp lý
  • Prototype, MVP, và product giai đoạn đầu

Khi nào nên tránh:

  • Workload high-throughput ổn định (rẻ hơn với server)
  • Long-running process (>15 phút)
  • WebSocket hoặc persistent connection requirement
  • Heavy compute (ML training, video processing)

Serverless không phải về loại bỏ server — mà về loại bỏ các quyết định về server. Để platform xử lý capacity, scaling, và availability trong khi bạn build thứ quan trọng.


Tiếp Theo Trong Software Architecture Series

Đây là bài 11 trong 12 bài của series Software Architecture Patterns:

Bài liên quan:

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.