Full Serverless Application — End-to-End Architecture
Why Build a Full Serverless Application?
Individual serverless services are powerful, but their true value comes from combining them into a complete architecture. This chapter walks through building a production-ready serverless application that integrates Lambda, API Gateway, DynamoDB, Step Functions, EventBridge, S3, and Cognito.
Why this matters for your career:
- Real-world serverless applications combine multiple AWS services
- Understanding the full architecture is essential for AWS certification
- Serverless architecture design is a common interview topic for cloud roles
- Building end-to-end serverless apps is a high-value freelance skill
Architecture Overview
┌──────────┐
│ Cognito │ (User authentication)
└─────┬────┘
│ JWT Token
┌─────▼────┐
Client (Browser) ──►│API Gateway│ (HTTP routing, auth, throttling)
└─────┬────┘
│
┌─────▼────┐
│ Lambda │ (Business logic)
└─────┬────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼────┐ ┌───▼───┐ ┌───▼────┐
│ DynamoDB │ │ Step │ │EventBr.│ (Database, orchestration, events)
└──────────┘ │Fnctns.│ └────────┘
└───────┘
│
┌─────▼────┐
│ S3 │ (File storage)
└──────────┘
Authentication (Cognito)
Amazon Cognito provides user sign-up, sign-in, and access control.
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: MyAppUsers
AutoVerifiedAttributes: [email]
Policies:
PasswordPolicy:
MinimumLength: 8
RequireUppercase: true
RequireLowercase: true
RequireNumbers: true
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: MyAppClient
UserPoolId: !Ref UserPool
GenerateSecret: false
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
API Layer (API Gateway + Lambda)
# SAM template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: python3.12
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
Resources:
# API Gateway (HTTP API)
MyApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
Authorizers:
CognitoAuthorizer:
IdentitySource: $request.header.Authorization
Jwt:
Issuer: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}
Audience:
- !Ref UserPoolClient
# Lambda Functions
CreateUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/create_user/
Handler: app.lambda_handler
Events:
CreateUser:
Type: HttpApi
Properties:
ApiId: !Ref MyApi
Path: /users
Method: POST
Auth:
Authorizer: CognitoAuthorizer
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/get_user/
Handler: app.lambda_handler
Events:
GetUser:
Type: HttpApi
Properties:
ApiId: !Ref MyApi
Path: /users/{id}
Method: GET
Auth:
Authorizer: CognitoAuthorizer
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
Database (DynamoDB)
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: createdAt
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: createdAt
KeyType: RANGE
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Workflow Orchestration (Step Functions)
OrderWorkflow:
Type: AWS::Serverless::StateMachine
Properties:
DefinitionUri: workflows/order-workflow.asl.json
Events:
OrderCreated:
Type: EventBridge
Properties:
EventBusName: default
Pattern:
source:
- myapp.orders
detail-type:
- OrderCreated
Policies:
- LambdaInvokePolicy:
FunctionName: !Ref ProcessPaymentFunction
- LambdaInvokePolicy:
FunctionName: !Ref UpdateInventoryFunction
- LambdaInvokePolicy:
FunctionName: !Ref SendNotificationFunction
Event-Driven Processing (EventBridge + DynamoDB Streams)
OrderStreamProcessor:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/order_stream_processor/
Handler: app.lambda_handler
Events:
Stream:
Type: DynamoDB
Properties:
Stream: !GetAtt OrdersTable.StreamArn
BatchSize: 100
StartingPosition: LATEST
Policies:
- DynamoDBReadPolicy:
TableName: !Ref OrdersTable
- SQSSendMessagePolicy:
QueueName: !Ref OrderDLQ
OrderDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: order-dlq
MessageRetentionPeriod: 1209600 # 14 days
File Storage (S3)
UploadsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: myapp-uploads-${AWS::AccountId}
CorsConfiguration:
CorsRules:
- AllowedOrigins: ['*']
AllowedMethods: [PUT, POST]
AllowedHeaders: ['*']
MaxAgeSeconds: 3600
LifecycleConfiguration:
Rules:
- Id: ExpireOldFiles
Status: Enabled
ExpirationInDays: 365
Deployment with SAM
# Build
sam build
# Deploy to AWS
sam deploy --guided
# Follow prompts: stack name, region, confirm changes, etc.
# Outputs:
# - API endpoint URL
# - Cognito User Pool ID
# - S3 bucket name
Complete Lambda Function Example
# functions/create_user/app.py
import json
import os
import uuid
from datetime import datetime
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
eventbridge = boto3.client('events')
def lambda_handler(event, context):
body = json.loads(event['body'])
user_id = str(uuid.uuid4())
item = {
'id': user_id,
'name': body['name'],
'email': body['email'],
'plan': body.get('plan', 'free'),
'createdAt': datetime.utcnow().isoformat() + 'Z',
'updatedAt': datetime.utcnow().isoformat() + 'Z'
}
table.put_item(Item=item)
# Emit event for downstream processing
eventbridge.put_events(Entries=[{
'Source': 'myapp.users',
'DetailType': 'UserCreated',
'Detail': json.dumps({'userId': user_id, 'plan': item['plan']}),
'EventBusName': 'default'
}])
return {
'statusCode': 201,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(item)
}
Testing the Full Application
# 1. Deploy
sam deploy
# 2. Get the API URL from SAM outputs
# OutputKey: MyApiEndpoint
# 3. Create a user via the API
curl -X POST https://api-id.execute-api.us-east-1.amazonaws.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <cognito-jwt-token>" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# 4. Get the user
curl https://api-id.execute-api.us-east-1.amazonaws.com/users/abc123 \
-H "Authorization: Bearer <cognito-jwt-token>"
# 5. Check CloudWatch Logs
aws logs describe-log-groups --log-group-name-prefix /aws/lambda/create-user
aws logs tail /aws/lambda/create-user --since 5m
# 6. Check DynamoDB
aws dynamodb scan --table-name UsersTable
# 7. Check Step Functions execution
aws stepfunctions list-executions --state-machine-arn <arn>
Best Practices
| Practice | Reason | |----------|--------| | Use AWS SAM or CDK for infrastructure | Repeatable, version-controlled deployments | | Apply least-privilege IAM policies | Security — only grant required permissions | | Use environment variables for configuration | No hardcoded values in Lambda code | | Enable X-Ray tracing | Debug distributed serverless applications | | Set up CloudWatch dashboards | Monitor API errors, Lambda throttles, DynamoDB throttling | | Implement structured logging | Easier log analysis and alerting | | Use dead letter queues for failures | Never lose failed events | | Enable DynamoDB auto-scaling | Handle traffic spikes without manual intervention |
Summary
A complete serverless application combines Cognito (auth), API Gateway (routing), Lambda (logic), DynamoDB (data), Step Functions (workflows), EventBridge (events), and S3 (storage). Use AWS SAM or CDK to define and deploy the entire stack as code.
Key takeaways:
- Cognito handles user authentication with JWT tokens
- API Gateway routes HTTP requests and enforces auth
- Lambda executes business logic with environment variables
- DynamoDB stores data with on-demand capacity for serverless
- Step Functions orchestrates multi-step workflows
- EventBridge enables event-driven communication
- S3 stores files and static assets
- Define everything with infrastructure as code (SAM/CDK)
- Enable X-Ray, CloudWatch, and DLQs for production readiness
- Use least-privilege IAM policies for security
What's Next: DevOps — GitOps
The next course covers GitOps — using Git as the single source of truth for infrastructure and application deployment with ArgoCD.