Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Intelligence View
⚡ tsecurity.de Intelligence

Scaling Java with Write-Behind Caching

If you ask a Junior Developer to build a URL Shortener (like Bit.ly) that tracks click analytics, the logic usually looks like this: User clicks the short link (/abc). App fetches the original URL from the database. App increments the…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

If you ask a Junior Developer to build a URL Shortener (like Bit.ly) that tracks click analytics, the logic usually looks like this:




  1. User clicks the short link (/abc).

  2. App fetches the original URL from the database.

  3. App increments the click counter in the database (UPDATE urls SET clicks = clicks + 1 ...).

  4. App redirects the user.



This works great for 100 users. It works okay for 1,000 users.

But at 10,000 requests per second (RPS)? Your database locks up, latency spikes, and your application crashes.



Why? Because Database I/O is expensive. Blocking a thread to write a "+1" to disk for every single click is a massive waste of resources.



In this article, I’ll show you how I solved this using the Write-Behind Caching pattern, Redis, and Java 25 Virtual Threads.







The Bottleneck: Synchronous Writes



In the naive approach, the "Read" (finding the URL) and the "Write" (tracking the click) are coupled.



If your database takes 50ms to perform the update, your maximum throughput per thread is capped. If traffic spikes, your connection pool exhausts, and requests start timing out.





The Solution: Write-Behind Caching



To achieve high scale, we need to decouple the analytics from the redirect. The user wants to go to Google.com; they don't care if we counted their click right now or 5 minutes from now.



Here is the architecture I implemented in my project:




  1. Fast Path: Increment the counter in Redis (Memory is fast!).

  2. Dirty Tracking: Add the Short Code to a "Dirty Set" in Redis so we know which records changed.

  3. Slow Path (Async): A background scheduler wakes up, reads the Dirty Set, and flushes the counts to the Database (DynamoDB/Postgres) in batches.



Let's look at the code.







Step 1: The "Fire and Forget" Increment



I used Spring Boot 4 and RedisTemplate. When a request comes in, we hit Redis. This operation takes sub-milliseconds.




@Async("clickExecutor")
public void incrementClickCount(String code) {
// 1. Atomic Increment in Redis (Super fast)
redisTemplate.opsForValue().increment("clicks:" + code, 1);

// 2. Mark this code as "Dirty" so the scheduler knows it changed
redisTemplate.opsForSet().add("dirty_urls", code);
}






Notice the @Async. The controller returns the redirect immediately. This logic ensures the user experiences zero latency penalty for our analytics tracking:




@Configuration
@EnableAsync
public class AsyncConfig {

@Bean(name = "clickExecutor")
public Executor clickExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
}









Step 2: The Virtual Thread Scheduler



This is where Java 25 shines.



Historically, background tasks that talk to databases were heavy because they blocked OS threads. With Project Loom (Virtual Threads), we can spawn threads that are lightweight. If the database is slow, the Virtual Thread "parks" itself, freeing up the carrier thread to do other work.



Here is the scheduler that runs every 5 minutes:




@Scheduled(cron = "0 */5 * * * *") // Runs every 5 mins
public void persistClicks() {
log.info("Starting sync job...");

while (true) {
// 1. Pop a batch of codes (e.g., 1000 at a time) from the Set
var codes = redisTemplate.opsForSet().pop("dirty_urls", 1000);

if (Objects.isNull(codes) || codes.isEmpty()) {
break;
}

// 2. Process in parallel using Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
codes.forEach(obj -> executor.submit(() -> syncToDb(obj.toString())));
}
}
}









Step 3: The Atomic Sync



The syncToDb method is responsible for moving the data from Cache to Storage.




private void syncToDb(String code) {
// 1. Get the count from Redis and RESET it to 0 atomically
var clickCountObj = redisTemplate.opsForValue().getAndSet("clicks:" + code, "0");

if (clickCountObj == null) return;
long clicks = Long.parseLong(clickCountObj.toString());

if (clicks > 0) {
// 2. Update the Database (in this case, DynamoDB)
repository.updateClickCount(code, clicks);
}
}

public void updateClickCount(String code, long totalClicks) {
dynamoDbClient.updateItem(req -> req
.tableName("urls")
.key(Map.of("code", AttributeValue.builder().s(code).build()))
.updateExpression("ADD clicks :inc")
.expressionAttributeValues(Map.of(
":inc",
AttributeValue.builder().n(String.valueOf(totalClicks)).build()
))
);
}










The Trade-off: What about Data Loss?



In distributed systems, everything is a trade-off. By using Write-Behind, we gain massive performance, but we introduce a risk: Durability.



If the Redis instance crashes completely before the 5-minute sync triggers, we lose the clicks that happened in that window.



Why is this acceptable?




  1. Business Value: Losing 5 minutes of click analytics is usually acceptable. Losing the actual URL mappings (Create operation) is not. (Creation is synchronous; Analytics is asynchronous).

  2. Mitigation: We can enable Redis AOF (Append Only File) persistence to minimize data loss on crash.






The Results



By implementing this pattern:




  • Latency: The GET /{code} endpoint response time dropped from ~60ms (DB write) to ~5ms (Redis write).

  • Throughput: The database is no longer hammered by every single click. It only receives bulk updates every few minutes.

  • Cost: We reduced Database Write Units (WCUs), which is the most expensive part of cloud databases like DynamoDB.






Conclusion



You don't always need microservices to scale. Sometimes, you just need to stop treating your database like a notepad. Patterns like Write-Behind Caching, combined with modern tools like Virtual Threads, allow monoliths to handle massive loads efficiently.



You can check out the full implementation (including the Docker Compose setup and Testcontainers integration) in the repository below:



GitHub Repo: Distributed URL Shortener

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Scaling Java with Write-Behind Caching

Thematisch verwandte Begriffe: Scaling, Java, with, WriteBehind · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY Kritische Sicherheitsmeldung
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick