CompletedGolanggRPCPostgreSQL+6 more

Tiny

High-performance, production-ready URL shortener in Go. Microservices with gRPC inter-service communication, PostgreSQL read replicas, Redis caching + streams, ClickHouse OLAP analytics, Snowflake ID generation, and a terminal UI client.

Timeline

Role

Status
Completed

Technology Stack

Golang
gRPC
PostgreSQL
Redis
ClickHouse
Docker
Kubernetes
Protocol Buffers
JWT

Tiny

A production-grade, distributed URL shortener built with Go microservices.

10,000+ lines of Go · 8 microservices · 6 data stores

Quick Start · API Docs · Architecture · Deep Dive

CI Go 1.25 gRPC License: MIT


What is Tiny?

Tiny is a full-stack URL shortener designed as a real-world distributed systems project. It goes far beyond a simple redirect service -- it includes user authentication, custom aliases, QR code generation, real-time click analytics with geo/device enrichment, distributed tracing, full-text search, and a terminal UI client.

Every architectural decision maps to a production concern: read replicas for scale, Redis Streams for async event processing, ClickHouse materialized views for sub-second analytics, Snowflake IDs for conflict-free distributed ID generation, and distributed locks for custom alias reservation.

Key Features

FeatureDescription
URL ShorteningAuto-generated short codes via Snowflake ID + Base62 encoding
Custom AliasesReserve vanity URLs with distributed lock protection
QR CodesAuto-generated QR code (Base64 PNG) for every short URL
Click AnalyticsReal-time tracking: geo location, device, browser, OS, referrer
User AccountsJWT authentication with registration, login, and profile management
Full-Text SearchSearch URLs via Elasticsearch across long URLs and short codes
TTL ExpirationConfigurable URL expiration with automated cleanup
TUI ClientInteractive terminal UI built with Bubble Tea
Distributed TracingEnd-to-end request tracing with Jaeger + OpenTelemetry
Multi-Tier CacheL1 (in-memory LRU) + L2 (Redis) for sub-millisecond redirects

Architecture

System Overview

Services

ServiceTypePortDescription
api-gatewayHTTP8080REST API, auth middleware, CORS, rate limiting, Swagger
redirect-serviceHTTP8081Fast 302 redirects with cache-first lookups
url-servicegRPC50051URL CRUD, Snowflake ID generation, custom aliases
user-servicegRPC50052Registration, login, JWT token management
analytics-workerWorker--Aggregates click events from Redis Streams to PostgreSQL
pipeline-workerWorker--Enriches clicks (GeoIP, UA parsing) and stores to ClickHouse + Elasticsearch
cleanup-workerWorker--Periodic deletion of expired URLs (every 24h)
tuiCLI--Interactive terminal client (Bubble Tea)

Redirect Flow (Hot Path)

Create URL Flow

Custom Alias Flow (with Distributed Locking)

Analytics Pipeline


Tech Stack

LayerTechnologyPurpose
LanguageGo 1.25All services
RPCgRPC + ProtobufInter-service communication
HTTPnet/httpAPI Gateway + Redirect Service
Primary DBPostgreSQL 16 (TimescaleDB)URLs, users, 1 primary + 3 read replicas
Analytics DBClickHouseClick events, materialized views for aggregations
Cache + QueueRedis 7Multi-tier cache (L1/L2), Streams for async events, rate limiting, distributed locks
SearchElasticsearch 8Full-text URL search, click event search, log shipping
TracingJaeger + OpenTelemetryDistributed request tracing across all services
LoggingZapStructured JSON logging with optional ES shipping
DI FrameworkUber FXDependency injection, lifecycle management, graceful shutdown
AuthJWT (golang-jwt/v5)Token-based authentication
ID GenerationSnowflake + Base62Globally unique, time-sortable, URL-safe short codes
QR Codesgo-qrcodePNG QR code generation (Base64-encoded)
GeoIPMaxMind GeoLite2IP-to-location enrichment
UA Parsingmssola/user_agentBrowser, OS, device detection
TUIBubble Tea (charmbracelet)Interactive terminal UI
ContainersDocker + BuildKitMulti-stage builds, layer caching
OrchestrationKubernetesDeployments, StatefulSets, HPAs, NetworkPolicies
CI/CDGitHub ActionsLint, test, vuln scan, Docker build, GHCR push

Quick Start

Prerequisites

  • Go 1.25+
  • Docker & Docker Compose
  • Make (optional, for convenience commands)

1. Clone and configure

git clone https://github.com/Varun5711/tiny.git
cd tiny
cp .env.example .env

2. Start infrastructure

# Start PostgreSQL (primary + 3 replicas), Redis, and ClickHouse
docker compose -f deployments/docker/docker-compose.yml up -d \
  postgres-primary postgres-replica1 postgres-replica2 postgres-replica3 \
  redis clickhouse

3. Start services

# Option A: Run all services with Docker Compose
docker compose -f deployments/docker/docker-compose.yml up --build

# Option B: Run services locally (requires Go 1.25+)
go run ./cmd/url-service &
go run ./cmd/user-service &
go run ./cmd/redirect-service &
go run ./cmd/api-gateway &
go run ./cmd/pipeline-worker &
go run ./cmd/analytics-worker &
go run ./cmd/cleanup-worker &

4. Try it out

# Register a user
curl -s -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"secret123","name":"Test User"}'

# Save the token from the response
TOKEN="<token-from-response>"

# Shorten a URL
curl -s -X POST http://localhost:8080/api/urls \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"long_url":"https://github.com/Varun5711/tiny"}'

# Visit the short URL
curl -v http://localhost:8081/<short_code>
# → 302 redirect to https://github.com/Varun5711/tiny

5. Launch the TUI

go run ./cmd/tui

API Reference

Authentication

Register

POST /api/auth/register
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword",
  "name": "John Doe"
}

Response 200 OK

{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "user@example.com",
  "name": "John Doe",
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Login

POST /api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword"
}

Get Profile

GET /api/auth/profile
Authorization: Bearer <token>

URLs

All URL endpoints require Authorization: Bearer <token> header.

Create Short URL

POST /api/urls
Content-Type: application/json

{
  "long_url": "https://example.com/very/long/path",
  "expires_at": 1735689600       // optional, unix timestamp
}

Response 201 Created

{
  "short_code": "7Bx9kL",
  "short_url": "http://localhost:8081/7Bx9kL",
  "long_url": "https://example.com/very/long/path",
  "created_at": 1704067200,
  "expires_at": 1735689600,
  "qr_code": "data:image/png;base64,iVBOR..."
}

Create Custom Alias

POST /api/urls/custom
Content-Type: application/json

{
  "alias": "my-brand",
  "long_url": "https://example.com",
  "expires_at": 1735689600       // optional
}

List URLs

GET /api/urls?limit=20&offset=0
Authorization: Bearer <token>

Response 200 OK

{
  "urls": [
    {
      "short_code": "7Bx9kL",
      "short_url": "http://localhost:8081/7Bx9kL",
      "long_url": "https://example.com",
      "clicks": 42,
      "created_at": 1704067200,
      "expires_at": 1735689600,
      "is_active": true
    }
  ],
  "total": 1,
  "has_more": false
}

Delete URL

DELETE /api/urls/{short_code}
Authorization: Bearer <token>

Redirect

GET http://localhost:8081/{short_code}
→ 302 Found (Location: https://original-url.com)

Analytics

Get URL Stats

GET /api/analytics/{short_code}/stats

Returns total clicks, unique visitors, last clicked timestamp.

Get Click Timeline

GET /api/analytics/{short_code}/timeline?period=7d

Returns hourly/daily click counts with unique visitor breakdowns.

Get Geo Stats

GET /api/analytics/{short_code}/geo

Returns click distribution by country with percentages.

Get Device Stats

GET /api/analytics/{short_code}/devices

Returns breakdown by device type, browser, and OS.

Get Top Referrers

GET /api/analytics/{short_code}/referrers

Returns ranked list of referrer URLs by click count.

Get Raw Click Events

GET /api/analytics/clicks?short_code={code}&limit=50&offset=0
Authorization: Bearer <token>

Search

Full-Text Search

GET /api/search?q=example&limit=10&offset=0

Searches across long URLs and short codes via Elasticsearch.


Health

GET /health
→ 200 OK    (PostgreSQL + Redis reachable)
→ 503        (dependency unavailable)

Configuration

All configuration is via environment variables (loaded from .env in development):

Database

VariableDefaultDescription
DB_PRIMARY_DSN--PostgreSQL primary connection string
DB_REPLICA1_DSN--Read replica 1
DB_REPLICA2_DSN--Read replica 2
DB_REPLICA3_DSN--Read replica 3
DB_MAX_CONNS25Max connections per pool
DB_MIN_CONNS5Min idle connections

Redis

VariableDefaultDescription
REDIS_ADDRlocalhost:6379Redis address
REDIS_PASSWORD--Redis password
REDIS_STREAM_NAMEclicks:streamStream name for click events

ClickHouse

VariableDefaultDescription
CLICKHOUSE_ADDRlocalhost:9000ClickHouse native protocol address
CLICKHOUSE_DATABASEanalyticsDatabase name
CLICKHOUSE_USERNAMEclickhouseUsername

Services

VariableDefaultDescription
API_GATEWAY_PORT8080API Gateway HTTP port
REDIRECT_SERVICE_PORT8081Redirect service HTTP port
BASE_URLhttp://localhost:8081Base URL for generated short links
DEFAULT_URL_TTL72hDefault URL expiration
JWT_SECRET--Required. Secret key for JWT signing

Elasticsearch

VariableDefaultDescription
ES_ENABLEDfalseEnable Elasticsearch integration
ES_ADDRESSEShttp://localhost:9200Comma-separated ES addresses
ES_INDEX_PREFIXshorternitIndex name prefix

Tracing

VariableDefaultDescription
TRACING_ENABLEDfalseEnable OpenTelemetry tracing
JAEGER_ENDPOINThttp://localhost:4318Jaeger OTLP endpoint
TRACING_SAMPLE_RATE1.0Sampling rate (0.0 to 1.0)

Rate Limiting

VariableDefaultDescription
RATE_LIMIT_REQUESTS100Max requests per window
RATE_LIMIT_WINDOW1mRate limit window duration

Cache

VariableDefaultDescription
CACHE_L1_CAPACITY10000In-memory LRU cache size
CACHE_L2_TTL1hRedis cache entry TTL

Project Structure

tiny/
├── cmd/                          # Service entry points
│   ├── api-gateway/              # HTTP REST API (Uber FX)
│   ├── redirect-service/         # Fast URL redirect (Uber FX)
│   ├── url-service/              # URL CRUD gRPC server (Uber FX)
│   ├── user-service/             # Auth gRPC server (Uber FX)
│   ├── analytics-worker/         # Redis Stream → PostgreSQL (Uber FX)
│   ├── pipeline-worker/          # Redis Stream → ClickHouse + ES (Uber FX)
│   ├── cleanup-worker/           # Expired URL deletion (Uber FX)
│   └── tui/                      # Terminal UI (Bubble Tea)
│
├── internal/                     # Private application packages
│   ├── analytics/                # Analytics aggregation service
│   ├── auth/                     # JWT manager + bcrypt passwords
│   ├── cache/                    # Multi-tier cache (LRU + Redis)
│   ├── clickhouse/               # ClickHouse client + analytics queries
│   ├── config/                   # Env-based configuration loader
│   ├── database/                 # PostgreSQL connection pool manager
│   ├── elasticsearch/            # ES client: URL index, click index, log shipping
│   ├── enrichment/               # GeoIP lookup + User-Agent parsing
│   ├── events/                   # Click event model + Redis Stream producer
│   ├── grpc/                     # gRPC client factory (with OTel instrumentation)
│   ├── handlers/                 # HTTP handlers (URL, Auth, Analytics, Swagger, Redirect)
│   ├── idgen/                    # Snowflake ID generator + Base62 encoder
│   ├── lock/                     # Redis-backed distributed lock (Lua script)
│   ├── logger/                   # Zap structured logging (JSON + ES syncer)
│   ├── middleware/               # CORS, rate limit, auth, recovery, tracing, request ID
│   ├── models/                   # Domain models (URL, User, errors)
│   ├── qrcode/                   # QR code PNG generation
│   ├── redis/                    # Redis client wrapper
│   ├── service/                  # Business logic (URL service, User service)
│   ├── storage/                  # PostgreSQL storage layer (CRUD, pagination, filters)
│   ├── tracing/                  # OpenTelemetry tracer provider setup
│   └── validation/               # Alias validation + alternative suggestions
│
├── proto/                        # Protobuf definitions
│   ├── url/                      # URL service (CreateURL, GetURL, ListURLs, DeleteURL, etc.)
│   ├── user/                     # User service (Register, Login, ValidateToken, etc.)
│   └── analytics/                # Analytics service
│
├── build/docker/                 # Multi-stage Dockerfiles (8 services)
├── deployments/
│   ├── docker/                   # docker-compose.yml (full stack)
│   ├── k8s/                      # Kubernetes manifests (base + overlays)
│   │   ├── base/                 # Deployments, Services, HPAs, NetworkPolicies
│   │   └── overlays/             # staging / production kustomizations
│   └── terraform/                # Infrastructure-as-code (placeholder)
│
├── scripts/
│   ├── databases/                # SQL schemas (PostgreSQL + ClickHouse)
│   ├── migrations/               # Database migration scripts
│   └── install.sh                # CLI installer (curl | bash)
│
├── docs/
│   ├── api/                      # gRPC API docs + examples
│   ├── architecture/             # System design + ADRs
│   └── deep-dive/                # 12-chapter technical deep dive
│
├── test/integration/             # End-to-end integration tests
├── api/openapi/                  # OpenAPI/Swagger specification
├── .github/workflows/ci.yaml    # CI pipeline (lint, test, build, Docker)
├── .env.example                  # Environment variable template
├── go.mod                        # Go module (30+ dependencies)
└── Makefile                      # Build automation

Database Schema

PostgreSQL

-- Users table
CREATE TABLE users (
    id            VARCHAR(50) PRIMARY KEY,
    email         VARCHAR(255) UNIQUE NOT NULL,
    name          VARCHAR(255) NOT NULL,
    password_hash TEXT NOT NULL,
    created_at    TIMESTAMPTZ DEFAULT NOW(),
    updated_at    TIMESTAMPTZ DEFAULT NOW()
);

-- URLs table
CREATE TABLE urls (
    short_code  VARCHAR(20) PRIMARY KEY,
    long_url    TEXT NOT NULL,
    user_id     VARCHAR(50),
    clicks      BIGINT DEFAULT 0,
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW(),
    expires_at  TIMESTAMPTZ,
    qr_code     TEXT
);

ClickHouse

CREATE TABLE analytics.click_events (
    event_id String, short_code String, original_url String,
    clicked_at DateTime64(3), ip_address String,
    country String, country_code String, region String, city String,
    latitude Float64, longitude Float64, timezone String,
    user_agent String, browser String, browser_version String,
    os String, os_version String, device_type String,
    device_brand String, device_model String,
    is_mobile UInt8, is_tablet UInt8, is_desktop UInt8, is_bot UInt8,
    referer String, query_params String
) ENGINE = MergeTree()
  PARTITION BY toYYYYMM(clicked_date)
  ORDER BY (short_code, clicked_at)
  TTL clicked_date + INTERVAL 180 DAY;

-- Pre-aggregated materialized views
-- daily_clicks_by_url, clicks_by_country, clicks_by_device, hourly_clicks

Deployment

Docker Compose (Development)

# Full stack: databases + all 7 services
docker compose -f deployments/docker/docker-compose.yml up --build

# Infrastructure only (bring your own services)
docker compose -f deployments/docker/docker-compose.yml up -d \
  postgres-primary postgres-replica1 postgres-replica2 postgres-replica3 \
  redis clickhouse

Kubernetes (Production)

# Apply base manifests
kubectl apply -k deployments/k8s/base/

# Or use overlays
kubectl apply -k deployments/k8s/overlays/production/

Includes:

  • Deployments with security contexts (runAsNonRoot, readOnlyRootFilesystem, drop ALL)
  • StatefulSets for PostgreSQL, Redis, ClickHouse, Elasticsearch
  • Horizontal Pod Autoscalers for all services
  • NetworkPolicies restricting traffic between services
  • Pod Disruption Budgets (production overlay)
  • CronJob for cleanup-worker
  • Ingress with path-based routing

CI/CD

The GitHub Actions pipeline runs on every push to main and refactor:

JobWhat it does
Lintgolangci-lint (errcheck, staticcheck, govet, etc.)
Testgo test -race with coverage
Vulngovulncheck (informational, non-blocking)
Buildgo build ./cmd/...
DockerBuild all 7 Docker images, push to GHCR on main

Development

Build all services

go build ./cmd/...

Run tests

# Unit tests
go test ./...

# With race detector
go test -race ./...

# Integration tests (requires running infrastructure)
INTEGRATION_TEST=true go test ./test/integration/ -v

Lint

golangci-lint run ./...

Generate Protobuf

protoc --go_out=. --go-grpc_out=. proto/**/*.proto

Design Decisions

DecisionChoiceWhy
ID generationSnowflake + Base62Time-sortable, no coordination needed, 7-char codes
Inter-service commgRPCType safety, streaming, smaller payload than JSON
Analytics storeClickHouseColumn-oriented, materialized views, 100x faster than PostgreSQL for aggregations
Event pipelineRedis StreamsBuilt-in consumer groups, at-least-once delivery, no Kafka overhead
Custom alias lockingRedis distributed lockPrevents race conditions; Lua-script-based release for safety
Caching strategyL1 (LRU) + L2 (Redis)Sub-millisecond L1 hits; L2 survives restarts
DI frameworkUber FXLifecycle hooks solve graceful shutdown; constructor injection catches missing deps at startup
Database replication1 primary + 3 replicasWrites to primary, reads distributed across replicas

Documentation

The docs/deep-dive/ directory contains a 12-chapter technical walkthrough:

  1. Big Picture -- System overview
  2. Database Architecture -- PostgreSQL replication, ClickHouse schema
  3. Messaging & Queuing -- Redis Streams pipeline
  4. Caching Strategy -- Multi-tier cache design
  5. Short Code Generation -- Snowflake + Base62
  6. gRPC Communication -- Service-to-service calls
  7. Authentication & JWT -- Auth flow
  8. Rate Limiting -- Redis-based sliding window
  9. Background Workers -- Pipeline, analytics, cleanup
  10. Code Walkthrough: Create URL
  11. Code Walkthrough: Redirect
  12. Scaling Strategy -- Horizontal scaling plan

License

MIT License -- see LICENSE for details.


Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Run tests (go test -race ./...)
  4. Run linter (golangci-lint run ./...)
  5. Commit your changes
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

Built by Varun Hotani

Developed by Varun Hotani
© 2026. All rights reserved.