Skip to main content

System Overview

The User Service follows a serverless, event-driven architecture on AWS. All components are managed services, eliminating operational overhead while providing automatic scaling. View full diagram

Component Breakdown

Client Layer

The client (web or mobile application) initiates all interactions. Authentication happens directly with Cognito, and all subsequent API calls include the JWT token.

Authentication Layer

AWS Cognito

Handles user registration, login, password reset, and MFA. Issues JWT tokens for authenticated sessions.

Cognito Authorizer

Attached to API Gateway. Validates JWT signature and expiry before any Lambda invocation.

API Layer

API Gateway serves as the single entry point:
  • Routes requests to appropriate Lambda handlers
  • Enforces authentication via Cognito Authorizer
  • Provides throttling and rate limiting
  • Handles CORS and request validation

Compute Layer

Lambda functions organized by domain:
Handler GroupResponsibility
User HandlersProfile CRUD, user status management
Email HandlersEmail CRUD, verification, primary designation
Cognito TriggersPost-confirmation hook to create DynamoDB record

Data Layer

DynamoDB with single-table design:
  • One table for all User Service entities
  • Global Secondary Index for email lookups
  • On-demand capacity for unpredictable workloads

Event Layer

EventBridge for inter-service communication:
  • Publishes domain events (user.created, email.verified, etc.)
  • Rules route events to interested consumers
  • Dead Letter Queue captures failed deliveries

Request Flow

1

Authentication

Client authenticates with Cognito, receives JWT access token
2

API Request

Client sends request to API Gateway with JWT in Authorization header
3

Token Validation

Cognito Authorizer validates JWT signature, expiry, and audience
4

Lambda Invocation

API Gateway invokes appropriate Lambda with user claims in context
5

Data Operation

Lambda performs DynamoDB operation, extracting userId from JWT claims
6

Event Publication

On state change, Lambda publishes event to EventBridge
7

Event Distribution

EventBridge routes event to subscribed downstream services

Why Serverless?

AspectBenefit
ScalingAutomatic scaling to zero and to peak demand
CostPay-per-invocation, no idle costs
OperationsNo servers to patch, update, or maintain
FocusEngineering time on business logic, not infrastructure

Technology Stack

ComponentTechnologyRationale
RuntimeNode.js 20.x (LTS)TypeScript support, fast cold starts, AWS SDK v3 native
FrameworkServerless FrameworkMature, well-documented, Cognito integration
DatabaseDynamoDBServerless, single-digit ms latency, scales infinitely
AuthCognitoManaged auth, JWT tokens, OAuth 2.0 flows
EventsEventBridgeServerless event bus, schema registry, filtering
IaCTerraform + ServerlessSplit responsibilities (see below)

Design Decisions

Key architectural choices and the reasoning behind them.
Options considered: AWS SAM, AWS CDK, Serverless Framework, Terraform-onlyChosen: Serverless FrameworkRationale:
  • More mature plugin ecosystem (offline, webpack, etc.)
  • Simpler YAML syntax compared to CDK’s programmatic approach
  • Better local development experience than SAM
  • Team familiarity reduces onboarding time
Trade-off: Less flexibility than CDK for complex infrastructure patterns
Options considered: SNS+SQS fan-out, EventBridge, direct Lambda invocationChosen: EventBridgeRationale:
  • Native content-based filtering (no need to filter in Lambda)
  • Schema Registry for event documentation
  • Archive and replay for debugging/recovery
  • Single event bus vs. managing multiple SNS topics
Trade-off: Slightly higher latency than direct SNS (~50-100ms), higher cost per event ($1/million vs SNS at $0.50/million)
Options considered: All Terraform, all Serverless, split approachChosen: Split - Terraform for shared resources, Serverless for Lambda lifecycleRationale:
  • Cognito and DynamoDB are long-lived; Lambda functions change frequently
  • Terraform state management better suited for infrastructure
  • Serverless Framework optimized for Lambda deployment patterns
  • Clear separation: Terraform outputs → SSM Parameters → Serverless reads
Integration pattern:
Terraform → SSM Parameter Store → Serverless Framework
(Cognito ARN)   (/user-service/prod/cognito-pool-id)   (${ssm:...})
Options considered: Separate User and Email tables, single table with entitiesChosen: Single-table designRationale:
  • Atomic transactions for primary email changes (TransactWriteItems requires same table)
  • Single query to fetch user + all emails
  • One table to monitor, backup, and scale
  • Cost efficiency (one set of on-demand capacity)
Trade-off: More complex access patterns, requires upfront design, harder to evolve
Options considered: Cognito sub directly, custom UUID with mapping tableChosen: Use Cognito sub directlyRationale:
  • No mapping table to maintain
  • Guaranteed unique across pool
  • Available in every JWT token
  • Simpler implementation
Trade-off: Harder to migrate away from Cognito (would need to remap all user IDs)