May 18, 2026·Karlo Hrvačić

How hrva.cc Handles Security, Speed, Reliability, and Migration

A deep dive into the architecture behind hrva.cc — how we keep links fast, safe, and always available through Redis caching, JWT auth, Safe Browsing, and zero-downtime migrations.

engineering
architecture
infrastructure
security

Building a URL shortener sounds simple: accept a long URL, generate a short code, redirect when someone visits it. But making that redirect fast, secure, and reliable at scale — while evolving the platform without downtime — is where the real engineering lives. Here's how hrva.cc is built.

Stack Overview

hrva.cc runs on a Spring Boot 4.0.6 backend (Java 25) with a Next.js 16 frontend (React 19). The backend uses PostgreSQL for persistent storage, Redis for caching, and MailHog for email in development. The frontend is deployed on Cloudflare Workers, and the backend runs in Docker with multi-arch builds (amd64 + arm64) via GitHub Actions.

Security: Defense in Depth

Threat Scanning at Every Level

Every URL shortened through hrva.cc is checked against the Google Safe Browsing API v4 before it's saved. If the destination is flagged as malware, phishing, or social engineering, the link is blocked at creation time. But threats evolve — a safe site today can be compromised tomorrow. That's why a scheduled task rechecks every active URL against Safe Browsing daily at 2 AM. Flagged URLs are automatically deactivated, evicted from cache, and the owner is notified by email.

JWT Authentication

All authenticated requests use stateless JWT tokens with configurable expiry (default 10 hours). Tokens are signed with a server-side secret and verified on every request through a custom JwtFilter that sits in the Spring Security chain. No session state, no server-side token storage — just cryptographic verification.

Rate Limiting

Login attempts are rate-limited using a Guava in-memory cache — 20 attempts per IP per day. After that, the IP is blocked until the window expires or an admin clears the limit from the dashboard. The admin panel also shows a live view of all active rate limits with a one-click clear button.

OWASP Basics

Actuator endpoints are locked down: /actuator/health is public, everything else requires ROLE_ADMIN. CORS is configured to only allow the frontend origin. The ForwardedHeaderFilter ensures correct IP resolution behind proxies. Input validation happens at the controller, service, and entity levels.

Speed: Every Millisecond Counts

Redis Caching

The redirect endpoint (GET /{short}) is the hottest path in the system — every click on every short URL goes through it. Initially it made two database queries per redirect. Now it's cache-first: on every request, we check Redis. If the URL data is cached and still valid (not expired, not over its visit limit, not deactivated), we serve the redirect from cache without touching the database. Visit tracking happens asynchronously in the background.

Cache Warmup and Priming

On application startup, the CacheWarmup component loads the 20 most visited, 20 most recent, and 20 most recently accessed URLs into Redis — so the most popular links are hot from the start. New URLs are cached immediately on creation via cacheUrlResponse(). The TTL is set to 7 days, but stale data is caught by validation logic on every cache hit. Redis graceful degradation means if Redis is down, we fall through to the database without error.

Read-Optimized Transactions

All service-layer methods default to @Transactional(readOnly = true). Only write operations explicitly override this. This gives the database query planner better optimization hints and prevents accidental writes in read paths.

Frontend Performance

The Next.js frontend uses server-side rendering for initial page loads, then takes over as a single-page app. The landing page is static and prerendered. Fonts are loaded via next/font with swap display. Animations use CSS-only keyframes (no JavaScript animation libraries) with zero external runtime dependencies for styling.

Reliability: Built to Handle Failures

Cache Graceful Degradation

Redis is a performance accelerator, not a hard dependency. A custom CacheErrorHandler catches every Redis failure — get, put, evict, clear — logs it, and returns null, causing the application to fall through to the database. A Redis outage means slightly slower requests, not downtime.

SafeBrowsing Graceful Degradation

The Google Safe Browsing API client is a singleton that retries on failure. If the API is unreachable, it returns an empty threat list — URLs are not blocked due to an API outage. The daily recheck task will catch any threats once the API is available again.

Async Visit Tracking

Visit counting is fully asynchronous. When someone clicks a short link, the redirect response is sent immediately, and a background task increments the visit counter using Spring's TaskExecutor. This means the redirect response time is independent of database write latency. Visit limits are checked on every redirect via the cached data, and the async write enforces the final deactivation.

Health Monitoring

The admin dashboard shows real-time status of PostgreSQL, Redis, JVM heap, uptime, and request counts. The system health page provides green/red indicators for every service layer. Micrometer metrics track redirect response times (cache hit vs. cache miss), and the admin panel displays average and max redirect latency.

Migration: Evolve Without Downtime

Liquibase for Schema Changes

All database changes go through Liquibase changelogs — versioned, sequential, and reversible. Migrations run automatically on application startup. The changelog has 9 migrations covering the full schema evolution from initial users table to audit logging and email tracking.

Angular to React Rewrite

The frontend was originally built in Angular 14. It was completely rewritten to Next.js 16 (React 19) in 2026. The rewrite was done alongside the existing Angular deployment, with the new frontend deployed to a separate subdomain (app.hrva.cc) while the old Angular app remained at the root domain. Once the new frontend was fully tested, the root domain was switched via a 302 redirect.

Gradle to Maven Migration

The backend was migrated from Gradle to Maven for faster builds and simpler CI configuration. This was a single-commit migration that touched only build files and directory structure — zero runtime code changes. The CI pipeline was updated simultaneously to use mvn package instead of gradle build.

Docker Multi-Arch CI

Every push to main triggers a GitHub Actions workflow that runs all 150+ backend tests, builds the application, and pushes a multi-arch Docker image (amd64 + arm64) to GitHub Container Registry. PRs run only the build and tests — no image push. Production deployments pull the image via Portainer with environment variables for OAuth2 secrets.

Deployment Safety

Rolling updates ensure zero-downtime deploys. Database schema changes via Liquibase are backward-compatible: new columns are added before code that reads them is deployed, and removed only after no running code references them. The scheduled SafeBrowsing recheck and URL deactivation tasks have configurable schedules in case maintenance windows are needed.

The Numbers

As of May 2026: 150+ automated tests covering controllers, services, validators, converters, and models. All tests run on every commit. Redis handles thousands of cache lookups with a 7-day TTL.