1 The Problem
We want a URL shortener: give it a long link, it returns a short code; give back the code, it returns the original link. It teaches two-way lookups (code↔URL), generating unique keys, and persisting a small store — the core of any link service.
2 How to Think About It
Two operations, one shared map: shortening picks a code and remembers what it points to; resolving looks the code back up and redirects there for real.
3 The Build — explained part by part
Here is the complete shortener. ConcurrentHashMap and a virtual-thread executor are both from the course’s Concurrency and Virtual Threads lessons, applied to a genuinely concurrent server rather than a toy example.
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
/**
* URL Shortener: maps short codes to long URLs in a thread-safe in-memory
* store, served over real HTTP on a virtual-thread-per-connection executor.
*/
public class UrlShortener {
private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final SecureRandom RANDOM = new SecureRandom();
static class Store {
private final ConcurrentHashMap<String, String> codeToUrl = new ConcurrentHashMap<>();
/** Retries until it finds a code not already in use, so collisions can never overwrite a URL. */
String shorten(String longUrl) {
String code;
do {
code = randomCode(6);
} while (codeToUrl.putIfAbsent(code, longUrl) != null);
return code;
}
String resolve(String code) {
return codeToUrl.get(code);
}
}
static String randomCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length())));
}
return sb.toString();
}
static HttpServer start(int port, Store store) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/shorten", exchange -> {
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8).trim();
if (body.isBlank()) {
exchange.sendResponseHeaders(400, -1);
exchange.close();
return;
}
String code = store.shorten(body);
byte[] resp = code.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, resp.length);
exchange.getResponseBody().write(resp);
exchange.close();
});
server.createContext("/", exchange -> {
String code = exchange.getRequestURI().getPath().substring(1);
String longUrl = store.resolve(code);
if (longUrl == null) {
exchange.sendResponseHeaders(404, -1);
} else {
exchange.getResponseHeaders().add("Location", longUrl);
exchange.sendResponseHeaders(307, -1);
}
exchange.close();
});
// Virtual threads: one per connection, cheap enough that this scales far past a fixed thread pool.
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
return server;
}
public static void main(String[] args) throws IOException {
start(8081, new Store());
System.out.println("Listening on http://localhost:8081/shorten");
}
}
HashMap — the JDK’s own thread-safe map, needed because the HTTP server can call shorten and resolve concurrently from different connections at once.codeToUrl.putIfAbsent(code, longUrl) != null inside a
do/while loop — putIfAbsent is atomic: it inserts only if the key is genuinely absent and tells you whether it did, in one call, with no separate check-then-insert race condition the way a plain containsKey followed by put would have.Executors.newVirtualThreadPerTaskExecutor() — from the course’s Virtual Threads lesson: each incoming connection gets its own cheap virtual thread instead of competing for a small fixed pool of expensive platform threads, letting this tiny server scale far past what a naive thread-per-connection design managed before Project Loom.
307 Temporary Redirect with a Location header, not
301 — a temporary redirect is the honest choice here since the short code’s mapping is not meant to be permanent or cached forever by the browser, unlike a real permanent URL change.
containsKey then calling put separately to avoid overwriting an existing code — two concurrent requests can both pass the check before either writes, both then overwriting the same code.putIfAbsent, as shorten does, which cannot race with itself.java.util.Random for the short code — predictable codes would let someone guess other users’ shortened URLs.SecureRandom, as randomCode does, the same choice the password generator project makes for the same reason.4 Test & Prove Each Part
We test the store's shorten/resolve logic directly, and separately prove the real server redirects correctly over actual HTTP.
import com.sun.net.httpserver.HttpServer;
import org.junit.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
public class UrlShortenerTest {
@Test
public void shortenThenResolveReturnsTheOriginalUrl() {
UrlShortener.Store store = new UrlShortener.Store();
String code = store.shorten("https://example.com/page");
assertEquals("https://example.com/page", store.resolve(code));
}
@Test
public void unknownCodeResolvesToNull() {
UrlShortener.Store store = new UrlShortener.Store();
assertEquals(null, store.resolve("nosuchcode"));
}
@Test
public void shorteningTheSameUrlTwiceGivesTwoDifferentCodes() {
UrlShortener.Store store = new UrlShortener.Store();
String a = store.shorten("https://example.com");
String b = store.shorten("https://example.com");
assertNotEquals(a, b);
assertEquals("https://example.com", store.resolve(a));
assertEquals("https://example.com", store.resolve(b));
}
@Test
public void randomCodeHasTheRequestedLength() {
assertEquals(6, UrlShortener.randomCode(6).length());
assertEquals(10, UrlShortener.randomCode(10).length());
}
@Test
public void realServerShortensAndRedirects() throws Exception {
UrlShortener.Store store = new UrlShortener.Store();
HttpServer server = UrlShortener.start(0, store);
try {
int port = server.getAddress().getPort();
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.build();
HttpRequest shortenReq = HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/shorten"))
.POST(HttpRequest.BodyPublishers.ofString("https://thecodex.expert/coding/"))
.build();
HttpResponse<String> shortenResp = client.send(shortenReq, HttpResponse.BodyHandlers.ofString());
assertEquals(200, shortenResp.statusCode());
String code = shortenResp.body();
HttpRequest resolveReq = HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/" + code))
.GET().build();
HttpResponse<Void> resolveResp = client.send(resolveReq, HttpResponse.BodyHandlers.discarding());
assertEquals(307, resolveResp.statusCode());
assertNotNull(resolveResp.headers().firstValue("Location").orElse(null));
assertEquals("https://thecodex.expert/coding/", resolveResp.headers().firstValue("Location").get());
} finally {
server.stop(0);
}
}
}
Compile and run with javac -cp junit-4.13.2.jar and hamcrest-core-1.3.jar UrlShortener.java UrlShortenerTest.java then java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore UrlShortenerTest. The last test starts a real server on an OS-assigned ephemeral port (port 0), makes real HTTP calls against it with HttpClient configured to not auto-follow redirects, and inspects the actual 307 status and Location header — genuinely exercising the virtual-thread executor, not just the in-memory store.
5 The Interface
What it expects
curl -X POST localhost:8081/shorten -d 'https://example.com/very/long/path'
curl -v localhost:8081/AbC123What it returns
AbC123
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/very/long/path6 Run It & Automate It
Save the code as UrlShortener.java and compile it with javac — that turns your source into .class bytecode files, which java then runs on the JVM. No separate install step: any real JDK ships both tools.
javac UrlShortener.java && java UrlShortenerThen shorten a URL and follow the redirect from another terminal, exactly as shown below.
A CI tool like Jenkins runs the same compile-then-test steps automatically whenever the code changes — every line below has a plain explanation.
$ java UrlShortener
Listening on http://localhost:8081/shorten
$ curl -X POST http://localhost:8081/shorten -d 'https://thecodex.expert/coding/'
M5tyI4
$ curl -D - -o /dev/null http://localhost:8081/M5tyI4
HTTP/1.1 307 Temporary Redirect
Location: https://thecodex.expert/coding/// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
environment {
CP = 'junit-4.13.2.jar:hamcrest-core-1.3.jar' // JUnit + its one dependency
}
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up JDK') {
steps {
sh 'java -version' // confirm a JDK is installed
sh 'javac -cp "$CP" *.java' // compile the program and its tests together
}
}
stage('Run the tests') {
steps {
sh 'java -cp ".:$CP" org.junit.runner.JUnitCore UrlShortenerTest'
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Add expiry. Store a creation time alongside each URL and reject codes older than a day. (Teaches:
Instant/Durationand a background cleanup task.) - Track click counts. Count how many times each code is resolved. (Teaches:
ConcurrentHashMap.computefor atomic read-modify-write updates.) - Persist across restarts. Combine this with the to-do list project’s file-based persistence approach. (Teaches: loading a ConcurrentHashMap’s contents from disk at startup.)
ConcurrentHashMap’s atomic putIfAbsent for genuinely race-free collision handling, a virtual-thread-per-connection executor for a server that scales past a fixed thread pool, and why a temporary (307) redirect is the honest choice for a mapping that is not permanent. Related: Virtual Threads, Concurrency.