Bittuly is a production-grade, distributed URL shortener built with Rust (Axum) and React (Vite). It uses a microservices architecture, separating authentication and URL management into distinct services with their own isolated databases.
auth-service(Port 3001): Handles user signups, stateless OTP verification (via email or console), JWT generation, and user management.url-service(Port 3002): Handles URL shortening, redirects, and click analytics tracking. Uses Redis for caching short URLs.consumer-service: Background asynchronous worker that consumes events from RabbitMQ (e.g., batch-updating clicks, cascading user deletion cleanups).libs/shared: Shared Rust crate containing RabbitMQ configurations, JWT logic, database connections, and middleware.Event Bus (RabbitMQ): Core messaging backbone providing At-Least-Once delivery for reliable, decoupled inter-service communication.web/(Port 5173): Modern React frontend built with Vite, Tailwind CSS, and a beautiful Notion-inspired design system.
To ensure the microservices remain decoupled, highly performant, and reliable, we use RabbitMQ as the central messaging backbone.
When a user deletes their account:
- Publisher (
auth-service): Deletes the user frompg-authand publishes aUserDeletedEvent(user_id)to RabbitMQ. It immediately returns204 No Contentto the user. - Consumer (
consumer-service): Asynchronously listens for the event. - Execution: The consumer connects to
pg-urlsto delete all links belonging to theuser_idusing aRETURNING short_codequery. It then loops through those codes and evicts them from the Redis Cache. - Resilience: If the
pg-urlsdatabase is temporarily down, the consumer utilizes Native Exponential Backoff. It publishes the message to tiered delay queues (3s, 9s, 27s) using RabbitMQ's Time-To-Live (TTL) feature. If it fails 4 times, it is routed to a permanent Dead Letter Queue (user_deleted_dlq).
When a user clicks a shortened URL:
- Publisher (
url-service): Responds with a fast302 Redirect(from Redis) and immediately fires ashort_codepayload into the RabbitMQclick_events_queuein the background. The user never waits for the database. - Consumer (
consumer-service): Pulls the click events off the queue and holds them in an in-memoryHashMap. - Execution (Batching): To prevent hammering the database, the consumer flushes the clicks to Postgres in bulk either every 30 seconds (Timer-based) or every 17 clicks (Size-based). It executes a single atomic
UPDATEusingunnestarrays. - Resilience: If the database is offline during a flush, the consumer sleeps its thread (Dynamic Backoff: 3s, 9s, 27s) and
NACKs the batch, cleanly applying backpressure to the RabbitMQ queue until the DB recovers.
The system requires two separate PostgreSQL databases and a Redis cache. These are fully containerized.
docker compose up -dThis spins up:
postgres-auth(Port 5432) β Database:bittuly_authpostgres-urls(Port 5433) β Database:bittuly_urlsredis(Port 6379)rabbitmq(Port 5672) and Management UI (Port 15672)
Note: The Postgres schemas are automatically created via the init scripts in docker/postgres-auth/init and docker/postgres-urls/init the first time the containers start.
Backend (/.env):
The root .env file configures the backend.
By default, MODE=development will bypass real SMTP emails and print your OTP code to the terminal.
To test real emails, set MODE=production and ensure SMTP_USER and SMTP_PASS are configured correctly.
Frontend (web/.env):
Ensure the frontend .env points to the correct backend ports:
VITE_AUTH_API_URL=http://localhost:3001
VITE_URLS_API_URL=http://localhost:3002We have set up convenient Cargo aliases using cargo-watch so that all services auto-reload on code changes. You will need three separate terminal windows for the backend.
(Ensure you have cargo-watch installed: cargo install cargo-watch)
Terminal 1 (Auth Service):
cargo dev-authTerminal 2 (URL Service):
cargo dev-urlsTerminal 3 (Consumer Service):
cargo dev-consumerOpen a third terminal window, navigate to the web/ directory, install dependencies, and start the Vite development server:
cd web
npm install
npm run devYour frontend is now available via the NGINX API Gateway at http://localhost:8000 (or http://localhost:5173 directly).
Bittuly is fully instrumented with OpenTelemetry, Prometheus, and Grafana for real-time traffic observability and distributed tracing.
The API gateway serves as the main entry point to the system, handling CORS, proxying, and caching interceptions.
- Frontend App:
http://localhost:8000 - Auth API:
http://localhost:8000/api/auth/* - URLs API:
http://localhost:8000/api/urls/*
Note: For security reasons, the raw /metrics endpoints are explicitly blocked (403 Forbidden) from being accessed through the public API gateway.
Prometheus runs entirely inside the internal Docker network (network_mode: "host") and scrapes the raw metrics from the Rust services every 5 seconds.
- UI Endpoint:
http://localhost:9090
Grafana visualizes the telemetry data collected by Prometheus.
- UI Endpoint:
http://localhost:3000 - Default Login:
admin/admin - Dashboards: Grafana is pre-provisioned with the "Bittuly Live Traffic" dashboard, featuring RPS, Latency, Cache Hits/Misses, and Business Metrics.
Jaeger collects and visualizes distributed traces using the OpenTelemetry Protocol (OTLP).
- UI Endpoint:
http://localhost:16686 - OTLP Receiver:
http://localhost:4317(Internal)
If you modify the SQL schema files and need to start fresh, you must destroy the Docker volumes and recreate them:
docker compose down -v
docker compose up -dcargo build --releaseThis builds optimized binaries for both services in the target/release folder.
This project uses GitHub Actions to automatically format, lint, and test both the Rust backend and React frontend on every push.
If you want to run these exact same checks locally before pushing, you can run the provided check script:
./scripts/check.shRecommended: Set up Git Hooks
To ensure your code is always formatted and passes checks before pushing, we recommend setting up two git hooks:
1. Pre-Commit Hook (Auto-formatting)
This hook automatically runs cargo fmt to format your Rust code right before creating a commit.
echo -e '#!/bin/sh\n\necho "==> Formatting Rust code (cargo fmt)..."\ncargo fmt --workspace\n\necho "==> Rust formatting complete."\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit2. Pre-Push Hook (CI Checks) This hook runs the full check script before pushing code to GitHub. If tests or linting fail, the push is aborted.
cp scripts/check.sh .git/hooks/pre-push
chmod +x .git/hooks/pre-push