Skip to main content

System Overview

The ADMA Cloud URL Shortener is a modern, cloud-native application built with a decoupled architecture that supports both local development and AWS production deployments. The system provides anonymous and authenticated URL shortening with real-time analytics and automated lifecycle management.
The platform supports two operational modes:
  • Anonymous Mode: Creates temporary links with an 8-hour TTL stored in localStorage
  • Authenticated Mode: Creates permanent links that persist in the database and can be managed by users

Architecture Diagrams

Browser

  ├─▶ http://localhost        → Frontend (Nginx :80)
  │                              └─ React SPA

  └─▶ http://localhost:8080   → Backend (Spring Boot :8080)
                                 └─ PostgreSQL (localhost:5432)
Local Stack Components:
  • Frontend: Nginx serving static React bundle
  • Backend: Spring Boot application with embedded Tomcat
  • Database: PostgreSQL 16 in Docker container
  • Orchestration: Docker Compose manages all services

Technology Stack

Backend

ComponentVersionPurpose
Java21 LTSRuntime platform with long-term support
Spring Boot3.4.2Application framework (Web, Security, Data JPA, Validation)
Hibernate6.6.5ORM layer with automatic schema management
JJWT0.12.6JWT token generation and validation (HS256)
BCryptStrength 12Password hashing per OWASP recommendations
PostgreSQL Driver42.7.5Database connectivity
ShedLock6.3.0Distributed locking for scheduled tasks
Gradle9.3.0Build automation and dependency management
Security Hardening: The backend uses BCrypt with a cost factor of 12 for password hashing, stateless JWT authentication, and environment-based secret management. See SecurityConfig.java:59

Frontend

ComponentVersionPurpose
React18UI framework with hooks and concurrent features
TypeScript5Type-safe development with strict mode
Vite5Fast build tool and dev server
React Router6Client-side routing
Tailwind CSS3Utility-first CSS framework
shadcn/uiLatestAccessible component library built on Radix UI
Framer MotionLatestAnimation library for smooth transitions
TanStack Query5Server state management and caching
BunLatestFast package manager and build runner
Build-Time Configuration: The frontend uses Vite’s environment variable system where VITE_API_BASE_URL is baked into the bundle at build time. This value cannot be changed at runtime and must be provided during the Docker image build.

Infrastructure

ServiceComponentPurpose
ECRElastic Container RegistryDocker image storage with vulnerability scanning
ECS FargateComputeServerless container orchestration (no EC2 management)
RDSDatabasePostgreSQL 16 with automated backups and Multi-AZ support
ALBLoad BalancerLayer 7 load balancing with path-based routing
ACMCertificate ManagerFree SSL/TLS certificates with auto-renewal
Route 53DNSDomain name resolution
Secrets ManagerSecrets StorageEncrypted storage for DB passwords and JWT secrets
CloudWatchLogging & MonitoringCentralized log aggregation and metrics
VPCNetworkingIsolated network with public/private subnets
VPC EndpointsPrivate ConnectivityInterface endpoints for ECR, CloudWatch, and Secrets Manager
Service DiscoveryCloud MapInternal DNS for backend service discovery

Component Interactions

Request Flow

1. User submits URL via frontend form
   └─> POST /api/urls/public

2. Backend creates ShortUrl entity
   ├─> linkType: ANONYMOUS
   ├─> expiresAt: now() + 8 hours
   └─> shortCode: 7-character random alphanumeric

3. Backend saves to PostgreSQL
   └─> Returns shortCode and expiresAt

4. Frontend stores in localStorage
   └─> Key: "anonymous_urls"
   └─> TTL: 8 hours (28,800,000ms)

5. User can share the short URL immediately

Data Flow

┌─────────────────────────────────────────────────────────────────┐
│  Frontend (React SPA)                                           │
│  ┌──────────────────┐        ┌──────────────────┐               │
│  │  AuthContext     │        │  localStorage    │               │
│  │  - JWT token     │◄──────►│  - JWT token     │               │
│  │  - User info     │        │  - Anonymous URLs│               │
│  └──────────────────┘        └──────────────────┘               │
│           │                                                      │
│           │ HTTP + JWT                                          │
└───────────┼──────────────────────────────────────────────────────┘

            │ HTTPS (443)

┌─────────────────────────────────────────────────────────────────┐
│  Application Load Balancer                                      │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │  Path-based routing:                                        ││
│  │  • /api/* → Backend Target Group                            ││
│  │  • /* → Frontend Target Group                               ││
│  └─────────────────────────────────────────────────────────────┘│
└───────────┬────────────────────────────┬────────────────────────┘
            │                            │
            ▼                            ▼
┌───────────────────────┐    ┌───────────────────────────────────┐
│  Backend Service      │    │  Frontend Service                 │
│  Spring Boot :8080    │    │  Nginx :80                        │
│  ┌─────────────────┐  │    │  ┌──────────────────────────────┐ │
│  │ Controllers     │  │    │  │  Static Assets               │ │
│  │ - Auth          │  │    │  │  - index.html                │ │
│  │ - ShortUrl      │  │    │  │  - JS bundle (Vite)          │ │
│  │ - Redirect      │  │    │  │  - CSS bundle (Tailwind)     │ │
│  │ - Stats         │  │    │  └──────────────────────────────┘ │
│  └────────┬────────┘  │    └───────────────────────────────────┘
│           │           │
│  ┌────────▼────────┐  │
│  │ Security Filter │  │
│  │ - JWT validation│  │
│  └────────┬────────┘  │
│           │           │
│  ┌────────▼────────┐  │
│  │ Service Layer   │  │
│  │ - ShortUrlSvc   │  │
│  │ - AuthService   │  │
│  └────────┬────────┘  │
│           │           │
│  ┌────────▼────────┐  │
│  │ JPA Repositories│  │
│  │ - ShortUrlRepo  │  │
│  │ - UserRepo      │  │
│  └────────┬────────┘  │
└───────────┼───────────┘
            │ JDBC

┌─────────────────────────────────────────────────────────────────┐
│  RDS PostgreSQL 16                                              │
│  ┌──────────────────┐  ┌──────────────────┐                     │
│  │  short_url       │  │  users           │                     │
│  │  - id            │  │  - id            │                     │
│  │  - short_code    │  │  - email         │                     │
│  │  - original_url  │  │  - password_hash │                     │
│  │  - user_id       │  │  - name          │                     │
│  │  - clicks        │  │  - created_at    │                     │
│  │  - link_type     │  └──────────────────┘                     │
│  │  - expires_at    │                                           │
│  │  - created_at    │  ┌──────────────────┐                     │
│  │  - avg_latency   │  │  shedlock        │                     │
│  └──────────────────┘  │  - name          │                     │
│                        │  - lock_until    │                     │
│                        │  - locked_at     │                     │
│                        └──────────────────┘                     │
└─────────────────────────────────────────────────────────────────┘

Key Architectural Patterns

Stateless JWT Authentication

The backend uses stateless JWT tokens for authentication, eliminating the need for server-side session storage:
// SecurityConfig.java:90
.sessionManagement(sm ->
    sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
Benefits:
  • Horizontal scalability (no session affinity required)
  • Reduced server memory footprint
  • Supports distributed deployments
  • Token contains all authentication context
JWT tokens are signed with HS256 using a secret stored in AWS Secrets Manager. The secret is automatically injected into the ECS task at runtime via the task definition’s secrets parameter.

Multi-Stage Docker Builds

Both frontend and backend use multi-stage Dockerfiles to minimize production image size:
# Stage 1: Build with full Gradle toolchain
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
RUN gradle dependencies --no-daemon || true
COPY src ./src
RUN gradle bootJar --no-daemon -x test

# Stage 2: Minimal runtime with JRE only
FROM eclipse-temurin:21-jre-alpine AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
Result: ~200MB runtime image (vs. ~800MB with full JDK)
# Stage 1: Build static assets
FROM node:20-alpine AS build
WORKDIR /app
ARG VITE_API_BASE_URL=http://localhost:8080
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
COPY package*.json ./
RUN npm install -g bun && bun install
COPY . .
RUN bun run build

# Stage 2: Serve with Nginx
FROM nginx:stable-alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/templates/default.conf.template
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Result: ~40MB runtime image (vs. ~400MB with Node.js)

Scheduled Job Coordination

The backend includes an automated cleanup service that runs every 15 minutes to remove expired anonymous links:
// BackendApplication.java:17
@EnableScheduling
public class BackendApplication {
    // ExpiredUrlCleanupService runs @Scheduled(cron = "0 */15 * * * *")
}
Production Consideration: The cleanup job uses ShedLock to ensure only one instance executes the task in multi-instance deployments. The lock is stored in a PostgreSQL table (shedlock) shared by all backend instances.

Distributed Locking with ShedLock

To prevent duplicate execution of scheduled tasks in scaled deployments:
// build.gradle:49
implementation 'net.javacrumbs.shedlock:shedlock-spring:6.3.0'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:6.3.0'
How it works:
  1. Before executing a scheduled task, ShedLock attempts to acquire a database lock
  2. If the lock is held by another instance, the task is skipped
  3. Lock duration ensures execution even if instance crashes
  4. Zero external dependencies (uses existing PostgreSQL)

Infrastructure as Code

The entire AWS infrastructure is defined using Terraform with a modular architecture:
infrastructure/terraform/
├── main.tf                    # Root orchestration
├── modules/
│   ├── network/              # VPC, subnets, routing
│   ├── security/             # Security groups
│   ├── ecr/                  # Container registries
│   ├── ecs/                  # Fargate services, ALB
│   ├── rds/                  # PostgreSQL database
│   ├── iam/                  # IAM roles and policies
│   └── vpc_endpoints/        # Private AWS service access
Key Features:
  • Precondition Validation: Enforces required variables before apply
  • Automatic Secret Generation: Creates secure random JWT secrets
  • Service Discovery: Backend accessible via Cloud Map private DNS
  • Auto-scaling: CPU and memory-based scaling policies for both services
  • Circuit Breakers: Automatic rollback on failed deployments

Security Architecture

Network Isolation

┌─────────────────────────────────────────────────────────────────┐
│  VPC (10.0.0.0/16)                                              │
│                                                                 │
│  ┌──────────────────────────┐  Public Subnets                  │
│  │  Internet Gateway        │  (10.0.1.0/24, 10.0.2.0/24)      │
│  └─────────┬────────────────┘                                  │
│            │                                                    │
│            ▼                                                    │
│  ┌──────────────────────────┐                                  │
│  │  Application Load Balancer│  Security Group: sg-ALB         │
│  │  - Ports: 80, 443         │  - Inbound: 0.0.0.0/0:80,443   │
│  └─────────┬────────────────┘  - Outbound: All                │
│            │                                                    │
│  ──────────┼────────── NAT Gateway                             │
│            │                                                    │
│            ▼                                                    │
│  Private Subnets (10.0.10.0/24, 10.0.11.0/24)                  │
│  ┌──────────────────────────┐  ┌──────────────────────────┐    │
│  │  Frontend Tasks          │  │  Backend Tasks           │    │
│  │  Security Group:         │  │  Security Group:         │    │
│  │  sg-FRONTEND             │  │  sg-BACKEND              │    │
│  │  - Inbound: sg-ALB:80    │  │  - Inbound: sg-ALB:8080  │    │
│  │  - Outbound: All         │  │  - Outbound: sg-RDS:5432 │    │
│  └──────────────────────────┘  └────────┬─────────────────┘    │
│                                          │                      │
│                                          ▼                      │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  RDS PostgreSQL                                           │ │
│  │  Security Group: sg-RDS                                   │ │
│  │  - Inbound: sg-BACKEND:5432                               │ │
│  │  - No public access                                       │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐ │
│  │  VPC Endpoints (PrivateLink)                              │ │
│  │  - com.amazonaws.region.ecr.api                           │ │
│  │  - com.amazonaws.region.ecr.dkr                           │ │
│  │  - com.amazonaws.region.s3 (gateway)                      │ │
│  │  - com.amazonaws.region.logs                              │ │
│  │  - com.amazonaws.region.secretsmanager                    │ │
│  └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Secrets Management

# docker-compose.yml
services:
  backend:
    environment:
      JWT_SECRET: ${JWT_SECRET:-changeme-this-must-be-at-least-32-chars-long!!}
      DB_PASSWORD: ${DB_PASSWORD:-changeme}
Secrets are stored in .env file (never committed to git).

Performance Optimizations

Welford’s Algorithm for Latency Tracking

The backend tracks average redirect latency using Welford’s online algorithm, which computes running averages without storing historical data:
μₙ = μₙ₋₁ + (xₙ − μₙ₋₁) / n

Where:
- μₙ = new average
- μₙ₋₁ = previous average
- xₙ = new measurement
- n = total count
Benefits:
  • O(1) memory usage (constant, regardless of click count)
  • O(1) computational complexity per update
  • Numerically stable for large datasets

Frontend State Management

The frontend uses TanStack Query (React Query) for efficient server state management:
// Features:
// - Automatic background refetching
// - Optimistic updates
// - Request deduplication
// - Cache invalidation strategies
// - Retry logic with exponential backoff

const { data: urls } = useQuery({
  queryKey: ['urls'],
  queryFn: fetchUrls,
  staleTime: 30000,      // Consider fresh for 30 seconds
  cacheTime: 300000,     // Keep in cache for 5 minutes
  refetchOnWindowFocus: true,
});

Database Indexing Strategy

Key indexes for optimal query performance:
-- Unique index on short_code (most frequent lookup)
CREATE UNIQUE INDEX idx_short_url_code ON short_url(short_code);

-- Index on user_id for dashboard queries
CREATE INDEX idx_short_url_user ON short_url(user_id);

-- Composite index for cleanup job
CREATE INDEX idx_short_url_expired ON short_url(link_type, expires_at)
  WHERE expires_at IS NOT NULL;

-- Index on email for login
CREATE UNIQUE INDEX idx_user_email ON users(email);

Monitoring and Observability

CloudWatch Integration

Log Groups:
├─ /ecs/adma-prod-frontend
│  └─ Nginx access/error logs
└─ /ecs/adma-prod-backend
   ├─ Spring Boot application logs
   ├─ SQL query logs (when enabled)
   └─ Exception stack traces
Log Retention: Configurable via ecs_log_retention_days (default: 30 days)

Health Checks

Frontend Health Check:
# Dockerfile health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget -q -O /dev/null http://localhost:80/ || exit 1
Backend Health Check:
# Uses Spring Boot Actuator
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \
  CMD wget -q -O /dev/null http://localhost:8080/actuator/health || exit 1
Target Group Health Checks:
  • Frontend: GET / expecting 200-399
  • Backend: GET /actuator/health expecting 200

Deployment Strategy

Blue-Green Deployments

ECS services are configured with deployment circuit breakers for safe updates:
deployment_circuit_breaker {
  enable   = true
  rollback = true
}

deployment_maximum_percent        = 200  # Allow 2x capacity during deploy
deployment_minimum_healthy_percent = 50   # Maintain at least 50% capacity
Deployment Flow:
  1. New task revision is created with updated container image
  2. ECS launches new tasks alongside existing tasks (up to 200% capacity)
  3. New tasks must pass health checks
  4. Traffic gradually shifts to new tasks
  5. Old tasks are drained and terminated
  6. If health checks fail, automatic rollback to previous revision

CI/CD Pipeline

# .github/workflows/deploy.yml highlights:

1. Build Phase:
   - Checkout code
   - Build Docker images with git SHA tags
   - Scan images for vulnerabilities (ECR)
   - Push to ECR with both :latest and :${{ github.sha }} tags

2. Deploy Phase:
   - Update ECS task definitions
   - Trigger service updates
   - Wait for deployment stability
   - Monitor health checks

3. Rollback:
   - If deployment fails, circuit breaker auto-reverts
   - Manual rollback: update service to previous task definition revision
Required GitHub Secrets:
  • AWS_ACCOUNT_ID: AWS account number
  • VITE_API_BASE_URL: Frontend API endpoint (build-time)

Scaling Characteristics

Horizontal Scaling

ComponentMinDesiredMaxScaling Metric
Frontend1210CPU > 70% OR Memory > 80%
Backend115CPU > 70% OR Memory > 80%
RDS-1-Manual (change instance class)
Backend Scaling Consideration: The scheduled cleanup job uses ShedLock to prevent concurrent execution. Scaling to multiple instances is safe and recommended for high availability.

Vertical Scaling

frontend_task_cpu    = 256   # 0.25 vCPU
frontend_task_memory = 512   # 512 MB
Typical resource usage: ~10% CPU, ~100MB memory

Cost Optimization

Resource Rightsizing

Monthly AWS Costs (us-east-1, approximate):
ServiceConfigurationMonthly Cost
ECS Fargate2 frontend tasks (0.25 vCPU, 512MB)~$15
ECS Fargate1 backend task (0.5 vCPU, 1GB)~$15
RDS PostgreSQLdb.t3.micro, 20GB gp3~$20
ALB1 ALB with minimal traffic~$20
Data TransferUnder 100GB/month~$10
CloudWatch Logs30-day retention, under 10GB~$5
Total~$85/month
Cost Optimization Strategies:
  • Use Savings Plans or Compute Savings Plans for ECS Fargate (up to 50% savings)
  • Enable RDS storage autoscaling to avoid over-provisioning
  • Set CloudWatch log retention to 7-14 days in development environments
  • Use VPC endpoints to avoid NAT Gateway data transfer charges ($0.045/GB)

High Availability

Multi-AZ Deployment

Availability Zone A              Availability Zone B
┌──────────────────────┐        ┌──────────────────────┐
│                      │        │                      │
│  Public Subnet       │        │  Public Subnet       │
│  ┌────────────────┐  │        │  ┌────────────────┐  │
│  │  ALB (active)  │◄─┼────────┼─►│  ALB (active)  │  │
│  └────────────────┘  │        │  └────────────────┘  │
│                      │        │                      │
│  Private Subnet      │        │  Private Subnet      │
│  ┌────────────────┐  │        │  ┌────────────────┐  │
│  │  ECS Tasks     │  │        │  │  ECS Tasks     │  │
│  │  - Frontend    │  │        │  │  - Frontend    │  │
│  │  - Backend     │  │        │  │  - Backend     │  │
│  └────────────────┘  │        │  └────────────────┘  │
│          │           │        │          │           │
│          ▼           │        │          ▼           │
│  ┌────────────────┐  │        │  ┌────────────────┐  │
│  │  RDS Primary   │◄─┼────────┼─►│  RDS Standby   │  │
│  │  (active)      │  │ Sync   │  │  (passive)     │  │
│  └────────────────┘  │ Repl.  │  └────────────────┘  │
└──────────────────────┘        └──────────────────────┘
High Availability Features:
  • ALB distributes traffic across availability zones
  • ECS tasks can run in multiple AZs
  • RDS Multi-AZ provides automatic failover (< 2 minutes)
  • Automated backups with point-in-time recovery

Disaster Recovery

Backup Strategy

db_backup_retention_days = 7  # Automated daily backups

# Point-in-time recovery:
# - Restore to any second within retention period
# - Transaction log backups every 5 minutes
Recovery Time Objective (RTO): ~30 minutes Recovery Point Objective (RPO): Under 5 minutes

Next Steps

Deployment Guide

Complete guide to deploying on AWS ECS Fargate

API Reference

Explore the REST API endpoints and authentication

Configuration

Configure environment variables and customize behavior

Build docs developers (and LLMs) love