Skip to main content
  1. Posts/

Part II - UrlShortener - first Implementation

·20 mins
Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

1. Introduction to implementation
#

1.1 Objectives and differentiation from the architectural part
#

The first part of this series focused on theory: We explained why a URL shortener is not just a convenience tool, but a security-relevant element of digital infrastructure. We discussed models for collision detection, entropy distribution, and forwarding logic, as well as analysed architectural variants – from stateless redirect services to domain-specific validation mechanisms.

  1. 1. Introduction to implementation
    1. 1.1 Objectives and differentiation from the architectural part
    2. 1.2 Technological guardrails: WAR, Vaadin, Core JDK
    3. 1.3 Overview of the components
  2. 2. Project structure and module organisation
    1. 2.1 Structure of a modular WAR project
    2. 2.2 Separation of domain, API and UI code
    3. 2.3 Tooling und Build (Maven, JDK 24, WAR-Plugin)
  3. 3. URL encoding: Base62 and ID generation
    1. 3.1 Design of a stable short link scheme
    2. 3.2 Implementation of a Base62 encoder
    3. 3.3 Alternatives: Random, Hashing, Custom Aliases
    4. 3.4 Implementation: Base62Encoder.java
    5. 3.5 What is happening here – and why?
  4. 4. Mapping Store: Storage of the mapping
    1. 4.1 Interface-Design: UrlMappingStore
    2. 4.2 In-memory implementation with ConcurrentHashMap
    3. 4.3 Extensibility for later persistence
    4. 4.4 Implementation
    5. 4.5 Why so?
  5. 5. HTTP API with Java tools
    1. 5.1 HTTP-Server mit com.sun.net.httpserver.HttpServer
    2. 5.2 POST /shorten: Shorten URL
    3. 5.3 GET /{code}: Redirect to the original URL
    4. 5.4 Implementation
      1. Starting point: ShortenerServer.java
      2. POST Handler: ShortenHandler.java
      3. GET-Handler: RedirectHandler.java
      4. JsonUtils.java (Minimal Java JSON without external libraries)
    5. 5.6 Core Java Client Implementation
    6. 5.7 Summary

This second part now turns to the concrete implementation. We develop a first working version of a URL shortener in Java 24 , consciously without the use of frameworks such as Spring Boot or Jakarta EE. The goal is to achieve a transparent, modularly structured solution that provides all core functions: URL shortening, secure storage of mappings, HTTP forwarding, and optional presentation via a Vaadin-based user interface.

Particular attention is paid to the clean separation between encoding, storage, API, and UI. The entire application is delivered as a monolithic artefact – specifically, a classic WAR file , which is compatible with standard servlet containers such as Jetty or Tomcat. This decision enables rapid deployment and facilitates onboarding and testability.

1.2 Technological guardrails: WAR, Vaadin, Core JDK
#

The implementation is based on a modern, yet deliberately lean technology stack. Only the built-in tools of the JDK and Vaadin Flow are used as the UI framework. The decision to use Vaadin is based on the requirement to implement interactive administration interfaces without additional JavaScript or separate front-end logic, entirely in Java.

The project is a multi-module structure. The separation between core logic, API layer, and UI remains visible and maintainable in the code. Maven is used as the build tool, supplemented by a WAR packaging plugin that creates a classic servlet deployment structure. The use of Java 24 enables the utilisation of modern language tools, including records, pattern matching, sequenced collections, and virtual threads.

The goal is a production-oriented, comprehensible implementation that can be used both as a learning resource and as a starting point for further product development.

1.3 Overview of the components
#

The application consists of the following core components:

  • A Base62-Encoder , which transforms consecutive IDs into URL-compatible short forms
  • A Mapping-Store , which manages the mapping between the short link and the original URL
  • A REST service , which allows URL shortening and resolution via redirect
  • One optional UI based on Vaadin Flow for manual management of mappings
    and a configurable WAR deployment that integrates all components

The architecture follows the principle: “As little as possible, as much as necessary.” Each part of the application is modular and allows for later splitting if necessary – for example, into separate services for reading, writing or analysis.

In the next chapter, we will focus on the concrete project structure and the module structure.

2. Project structure and module organisation
#

2.1 Structure of a modular WAR project
#

The first executable version of the URL shortener is realised as a monolithic Java application, which is in the form of a classic WAR.The project’s structure is based on a clear,layered architecture , which is prepared for later decomposition. The project is organised modularly, distinguishing between core logic, HTTP interface, and user interface. This separation not only allows for better maintainability but also forms the basis for the service decomposition planned in Part III or IV.

The project consists of three main modules:

  • shortener-core: Contains all business logic, including URL encoding, data model and store interfaces.
  • shortener-api: Implements the REST API based on the Java HTTP server (com.sun.net.httpserver.HttpServer).
  • shortener-ui-required: Optional UI module with Vaadin Flow for managing and visualising mappings.

These modules are distributed via a central WAR project (shortener war), which handles the delivery configuration and combines all dependencies. The WAR project is the only one that handles servlet-specific aspects (e.g., web.xml, I require a Servlet) – the remaining modules remain entirely independent of it.

2.2 Separation of domain, API and UI code
#

2.2 Separation of Domain, API, and UI Code

The modularisation of the project is based on the principle of technological isolation: The core business logic must know nothing about HTTP, servlet containers, or UI frameworks. This way, it remains fully testable, interchangeable, and reusable—for example, for future CLI or event-based variants of the shortener.

The core module defines all central interfaces (UrlMappingStore, ShortCodeEncoder) as well as the base classes (ShortUrlMapping, Base62Encoder). These components do not contain any I/O logic.

The api module is responsible for parsing HTTP requests, routing, and generating redirects and JSON responses. It accesses the core logic internally but remains detached from UI aspects.

The ui-vaadin module uses Vaadin Flow to implement a web-based interface, integrates the core logic directly, and is initialised in the WAR via a dedicated servlet definition.

Additional modules can be optionally added—for example, for persistence, monitoring, or analysis—without compromising the coherence of the structure.

2.3 Tooling und Build (Maven, JDK 24, WAR-Plugin)
#

The build system is based on the current version of Maven. Each module is managed as a standalone Maven project with its pom.xml, with shortener-war configured as the parent WAR application. WAR packaging is handled using the standard servlet model, allowing the resulting file to be easily deployed in Tomcat, Jetty, or any Servlet 4.0+ compatible container.

Java 24 is required at runtime, which is particularly relevant for modern language features such as record, pattern matching, and SequencedMap. Release 21 or higher is recommended as the target platform to ensure compatibility with modern runtimes.

Vaadin Flow integration is handled purely on the server side via the Vaadin Servlet and does not require a separate front-end build pipeline. Resources such as themes and icons are loaded entirely from the classpath.

3. URL encoding: Base62 and ID generation
#

3.1 Design of a stable short link scheme#

The key requirement for a URL shortener is to generate unique, shortest possible character strings that serve as keys for accessing the original URL. To meet this requirement, the first implementation utilises a sequential ID scheme that assigns a consecutive numeric ID to each new URL. This ID is then converted into a URL-compatible format—specifically, Base62.

Base62 includes the 26 uppercase letters, the 26 lowercase letters, and the 10 decimal digits. Unlike Base64, Base62 does not contain special characters such as +, /, or =, making it ideal for URLs: The generated strings are readable, system-friendly, and easily transferable in all contexts.

The resulting scheme is thus based on a two-step process:

  • Assigning a unique numeric ID (e.g., 1, 2, 3, …)
  • Converting this ID to a Base62 string (e.g., 1 → b, 2 → c, …)

This method guarantees unique and unguessable codes, especially if the ID count does not start at 0 or if codes are additionally shuffled.

3.2 Implementation of a Base62 encoder
#

The Base62-Encoder is used as a standalone utility class in the core module. It contains two static methods:

  • encode(long value): converts a positive integer to a Base62 string
  • decode(String input): converts a Base62 string back to an integer

The alphabet is defined internally as a constant character string, and the conversion process is carried out purely mathematically, comparable to the representation of a number in another place value system.

This implementation creates a deterministic, stable, and thread-safe encoder that requires no external libraries. The resulting codes are significantly shorter than the underlying decimal number and contain no special characters—a key advantage for embedded or typed links.

For exceptional cases—such as custom aliases—the encoder remains optional, as such aliases can be stored directly as separate strings. However, by default, the Base62 encoder is the preferred method.

3.3 Alternatives: Random, Hashing, Custom Aliases
#

In addition to the sequential approach, there are other methods for generating short links that can be considered in later stages of development:

  • Random-based tokens (z. B. UUID, SecureRandom) increase unpredictability, but require collision detection and additional memory overhead.
  • The hashing process (e.g., SHA-1 of the destination URL) guarantees stability but is prone to collisions under high load or identical destination addresses.
  • Custom aliases enable readable, short links (e.g., /helloMax), but require additional checking for collisions, syntactic validity, and protection of reserved terms.

For the first version, we focus on the sequential model with Base62 transformation – a stable and straightforward approach.

3.4 Implementation: Base62Encoder.java
#

The goal is to provide a simple utility class that converts integers to Base62 strings and vice versa. This class is thread-safe, stateless, and implemented without any external dependencies.

First, the complete source code:

public final class Base62Encoder {
 private static final String ALPHABET 
                 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 private static final int BASE = ALPHABET.length();
 private Base62Encoder() {
 }
 public static String encode(long number) {
   if (number < 0) {
     throw new IllegalArgumentException("Only non-negative values supported");
   }
   StringBuilder result = new StringBuilder();
   do {
     int remainder = (int) (number % BASE);
     result.insert(0, ALPHABET.charAt(reminder));
     number = number / BASE;
   } while (number > 0);
   return result.toString();
 }
 public static long decode(String input) {
   if (input == null || input.isEmpty()) {
     throw new IllegalArgumentException("Input must not be null or empty");
   }
   long result = 0;
   for (char c : input.toCharArray()) {
     int index = ALPHABET.indexOf(c);
     if (index == -1) {
       throw new IllegalArgumentException("Invalid character in Base62 string: " + c);
     }
     result = result * BASE + index;
   }
   return result;
 }
}

3.5 What is happening here – and why?
#

This class encapsulates all Base62 encoding behaviour in two static methods. The character set consists of digits (0–9), lowercase letters (a–z), and uppercase letters (A–Z), resulting in exactly 62 different characters.

  • The method encode(long number) converts an integer into its inverse representation in the base 62 place value system. The remainder of the division by 62 is successively calculated, and the corresponding character is inserted. The result is a short, URL-friendly string.

  • The decode(String input) method reverses this process: it converts a Base62 string back into its numeric representation. Each character is replaced by its index in the alphabet and weighted accordingly.

This implementation is robust against invalid input, operates entirely in memory, and can be used directly in ID generation or URL mappings.

Implementation: ShortCodeGenerator.java

The class abstracts ID generation from the concrete storage mechanism. It is suitable for both the in-memory version and future persistent variants. The generator is thread-safe and uses only JDK resources.

public final class ShortCodeGenerator {
 private final AtomicLong counter;
 public ShortCodeGenerator(long initialValue) {
   this.counter = new AtomicLong(initialValue);
 }
 public String nextCode() {
   long id = counter.getAndIncrement();
   return Base62Encoder.encode(id);
 }
 public long currentId() {
   return counter.get();
 }
}

Explanation

This class encapsulates a sequential counter that is incremented each time nextCode() generates a new, unique short code. The output is based on a monotonically increasing ID encoded into a Base62 string.

The getAndIncrement() method of AtomicLong is non-blocking and thus highly performant, even under high concurrency conditions. The generated code is unambiguous, compact, and deterministic—properties that are ideal for auditing, logging, and subsequent analysis.

The constructor allows you to configure the initial value. This is useful, for example, if you want to continue a persistent counter after a restart.

Example usage (e.g. in the mapping store)

ShortCodeGenerator generator = new ShortCodeGenerator(1L);
String shortCode = generator.nextCode(); // z.B. "b", "c", "d", ...

4. Mapping Store: Storage of the mapping
#

4.1 Interface-Design: UrlMappingStore
#

The mapping store forms the heart of the URL shortener. It manages the mapping between a short code (e.g.kY7zD) and the corresponding target URL (https://example.org/foo/bar). At the same time, it takes control over multiple uses, expiration times, and potential aliases.

In the first stage of development, a purely in-memory-based solution is used**.** This is fast, simple, and ideal for starting—even if it is lost upon reboot. Persistence is deliberately postponed to a later phase.

The store is abstracted via a simple interface. This interface allows for later substitution (e.g., with a file- or database-based version) without affecting the API or UI components.

4.2 In-memory implementation with ConcurrentHashMap
#

The first concrete implementation utilises a ConcurrentHashMap to ensure reliable access even under heavy load. Each mapping entry is represented by a simple record object (ShortUrlMapping) that contains the target URL, creation time, and optional expiration information.

The combination of ConcurrentHashMap and ShortCodeGenerator allows deterministic and thread-safe ID assignment without the need for explicit synchronisation. This creates a high-performance solution that operates reliably even under high load.

4.3 Extensibility for later persistence
#

All entries are accessible via a central interface. This interface is used not only for storage and retrieval but also forms the basis for later extensions, such as a persistence layer with flat files or EclipseStore, process control via TTL, or even event-driven backends.

The data structure can be extended with metrics, validity logic, or audit fields without requiring changes to the API – a classic approach to interface orientation in the sense of the open/closed principles.

4.4 Implementation
#

public record ShortUrlMapping(
   String shortCode,
   String originalUrl,
   Instant createdAt,
   Optional<Instant> expiresAt
) {}

This structure represents the basic assignment and allows the optional specification of an expiration time.

Store-Interface: UrlMappingStore.java

public interface UrlMappingStore {
 ShortUrlMapping createMapping(String originalUrl);
 Optional<ShortUrlMapping> findByShortCode(String shortCode);
 boolean exists(String shortCode);
 List<ShortUrlMapping> findAll();
 boolean delete(String shortCode);
 int mappingCount();
}

The interface is deliberately kept slim and abstracts the two core operations: insert (with creation) and lookup.

Implementation: InMemoryUrlMappingStore.java

public class InMemoryUrlMappingStore
   implements UrlMappingStore, HasLogger {
 private final ConcurrentHashMap<String, ShortUrlMapping> store 
                    = new ConcurrentHashMap<>();
 private final ShortCodeGenerator generator;
 public InMemoryUrlMappingStore() {
   this.generator = new ShortCodeGenerator(1L);
 }
 @Override
 public ShortUrlMapping createMapping(String originalUrl) {
   logger().info("originalUrl: {} ->", originalUrl);
   String code = generator.nextCode();
   ShortUrlMapping shortMapping = new ShortUrlMapping(
       code,
       originalUrl,
       Instant.now(),
       Optional.empty()
   );
   store.put(code, shortMapping);
   return  shortMapping;
 }
 @Override
 public Optional<ShortUrlMapping> findByShortCode(String shortCode) {
   return Optional.ofNullable(store.get(shortCode));
 }
 @Override
 public boolean exists(String shortCode) {
   return store.containsKey(shortCode);
 }
 @Override
 public List<ShortUrlMapping> findAll() {
   return new ArrayList<>(store.values());
 }
 @Override
 public boolean delete(String shortCode) {
   return store.remove(shortCode) != null;
 }
 @Override
 public int mappingCount() {
   return store.size();
 }
}

4.5 Why so?
#

The use of a ConcurrentHashMap ensures that concurrent write and read operations can be handled consistently and efficiently. The combination with AtomicLong in ShortCodeGenerator prevents collisions. The interface allows for a persistent implementation to be introduced later without changing the API or UI behaviour.

5. HTTP API with Java tools
#

5.1 HTTP-Server mit com.sun.net.httpserver.HttpServer
#

Instead of relying on heavyweight frameworks like Spring or Jakarta EE, we use the lightweight HTTP server implementation that the JDK already includes in the package com.sun.net.httpserver. This API, although rudimentary, is performant, stable, and perfectly sufficient for our use case.

The server is configured in just a few lines, requires no XML or annotation-based mappings, and can be controlled entirely programmatically. For each path, we define a separate HTTP handler that receives the request, processes it, and returns a structured HTTP response.

5.2 POST /shorten: Shorten URL
#

The first endpoint allows a long URL to be passed over an HTTP POST to the shortener. In response, the server returns the generated short form, in the simplest case as a JSON object with the shortCode.

Example request:

POST /shorten

Content-Type: application/json

{
  "url": "https://example.com/some/very/long/path"
}

Answer:

200 OK

Content-Type: application/json

{
  "shortCode": "kY7zD"
}

Missing or invalid entries are marked with 400 Bad Request answered.

5.3 GET /{code}: Redirect to the original URL
#

When calling a shortcode (e.g.GET /kY7zD), the server checks whether a mapping exists. If so, a HTTP 302 redirect to the original address. If the code is unknown or expired, a 404 Not Found error will be displayed.

This redirection is stateless and allows for later isolation into a read-only redirect service.

5.4 Implementation
#

Starting point: ShortenerServer.java
#

public class ShortenerServer
   implements HasLogger {
 private HttpServer server;
 public static void main(String[] args)
     throws IOException {
   new ShortenerServer().init();
 }
 public void heat()
     throws IOException {
   our store = new InMemoryUrlMappingStore();
   this.server = HttpServer.create(new InetSocketAddress(8080), 0);
   server.createContext("/shorten", new ShortenHandler(store));
   server.createContext("/", new RedirectHandler(store));
   server.setExecutor(null); // default executor
   server.start();
   System.out.println("URL Shortener server running at http://localhost:8080");
 }
 public void shutdown() {
   if (server != null) {
     server.stop(0);
     System.out.println("URL Shortener server stopped");
   }
 }
}

POST Handler: ShortenHandler.java
#

public class ShortenHandler
   implements HttpHandler, HasLogger {
 private final UrlMappingStorestore;
 public ShortenHandler(UrlMappingStore store) {
   this.store = store;
 }
 @Override
 public void handle(HttpExchange exchange)
     throws IOException {
   if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
     exchange.sendResponseHeaders(405, -1);
     return;
   }
   InputStream body = exchange.getRequestBody();
   Map<String, String> payload = parseJson(body);
   String originalUrl = payload.get("url");
   logger().info("Received request to shorten url: {}", originalUrl);
   if (originalUrl == null || originalUrl.isBlank()) {
     exchange.sendResponseHeaders(400, -1);
     return;
   }
   ShortUrlMapping mapping = store.createMapping(originalUrl);
   logger().info("Created mapping for {} -> {}", originalUrl, mapping.shortCode());
   byte[] response = toJson(Map.of("shortCode", mapping.shortCode())).getBytes(StandardCharsets.UTF_8);
   exchange.getResponseHeaders().add("Content-Type", "application/json");
   exchange.sendResponseHeaders(200, response.length);
   try (OutputStream os = exchange.getResponseBody()) {
     os.write(response);
   }
 }
}

GET-Handler: RedirectHandler.java
#

public class RedirectHandler
   implements HttpHandler , HasLogger {
 private final UrlMappingStorestore;
 public RedirectHandler(UrlMappingStore store) {
   this.store = store;
 }
 @Override
 public void handle(HttpExchange exchange)
     throws IOException {
   our requestURI = exchange.getRequestURI();
   our fullPath = requestURI.getPath();
   logger().info("Full path: {}", fullPath);
   String path = fullPath.substring(1); // strip leading '/'
   logger().info("Path: {}", path);
   if (path.isEmpty()) {
     exchange.sendResponseHeaders(400, -1);
     return;
   }
   Optional<String> target = store
       .findByShortCode(path)
       .map(ShortUrlMapping::originalUrl);
   if (target.isPresent()) {
     exchange.getResponseHeaders().add("Location", target.get());
     exchange.sendResponseHeaders(302, -1);
   } else {
     exchange.sendResponseHeaders(404, -1);
   }
 }
}

JsonUtils.java (Minimal Java JSON without external libraries)
#

Since we do not want to use external dependencies such as Jackson or Gson in this first implementation, we need our own utility class to process simple JSON objects, specifically:

  • String → Map<String, String>: for processing HTTP POST payloads (/shorten)
  • Map<String, String> → JSON-String: to generate responses (e.g.{ “shortCode”: “abc123” })

This class is sufficient for simple key-value structures, as used in the shortener. It is not intended for nested objects or arrays , but as a pragmatic solution for the start.

Implementation: JsonUtils.java

public final class JsonUtils {
 private JsonUtils() { }
 public static Map<String, String> parseJson(InputStream input)
     throws IOException {
   String json = readInputStream(input).trim();
   return parseJson(json);
 }
 @NotNull
 public static Map<String, String> parseJson(String json)
     throws IOException {
   if (!json.startsWith("{") || !json.endsWith("}")) {
     throw new IOException("Invalid JSON object");
   }
   Map<String, String> result = new HashMap<>();
   // Remove curly braces
   String body = json.substring(1, json.length() - 1).trim();
   if (body.isEmpty()) {
     return Collections.emptyMap();
   }
   // Separate key-value pairs with commas
   String[] entries = body.split(",");
   Arrays.stream(entries)
       .map(entry -> entry.split(":", 2))
       .filter(parts -> parts.length == 2)
       .forEachOrdered(parts -> {
         String key = unquote(parts[0].trim());
         String value = unquote(parts[1].trim());
         result.put(key, value);
       });
   return result;
 }
 public static String toJson(Map<String, String> map) {
   StringBuilder sb = new StringBuilder();
   sb.append("{");
   boolean first = true;
   for (Map.Entry<String, String> entry : map.entrySet()) {
     if (!first) {
       sb.append(",");
     }
     sb.append("\"").append(escape(entry.getKey())).append("\":");
     sb.append("\"").append(escape(entry.getValue())).append("\"");
     first = false;
   }
   sb.append("}");
   return sb.toString();
 }
 private static String readInputStream(InputStream input)
     throws IOException {
   try (BufferedReader reader = new BufferedReader(
       new InputStreamReader(input, StandardCharsets.UTF_8))) {
     return reader.lines().collect(joining());
   }
 }
 private static String unquote(String s) {
   if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) {
     return s.substring(1, s.length() - 1);
   }
   return s;
 }
 private static String escape(String s) {
   // simple escape logic for quotes
   return s.replace("\"", "\\\"");
 }
}

Properties and limitations

This implementation is:

  • fully JDK-based(no third-party libraries)
  • for flat JSON objects suitable, i.e.{ “key”: “value” }
  • robust against trivial parsing errors , but without JSON Schema validation
  • Consciously minimalistic to stay within the scope of the prototype

It is sufficient for:

  • POST /shorten(Client sends{ “url”: “…” })
  • Response to this POST (server sends{ “shortCode”: “…” })

Example use

InputStream body = exchange.getRequestBody();
Map<String, String> input = JsonUtils.parseJson(body);
String shortCode = "abc123";
String response = JsonUtils.toJson(Map.of("shortCode", shortCode));

For productive systems, the following is recommended in the future:

  • Gson(lightweight, idiomatic)
  • Jackson(extensive, also for DTO binding)
  • Json-B(Standard-API, Jakarta conform)

However, for our first implementation in the Core JDK, the solution shown above deliberately remains the appropriate middle ground.

5.6 Core Java Client Implementation
#

The class URLShortenerClient functions as a minimalist HTTP client for interacting with a URL shortener service. Its structure allows connection to a configurable or locally running server, with the default address being http://localhost:8080/. This enables easy integration into local development environments, test runs, or automated system tests without the need for additional configuration.

At the heart of the functionality is the method shortenURL(String originalUrl). It initiates an HTTP POST call against the server endpoint/shorten, transmits the URL to be shortened in a simple JSON document and immediately evaluates the server’s response. Successful completion is indicated exclusively by a status code.200 OK In this case, the method extracts the contained shortCode using the static auxiliary method extractShortCode() from the class JsonUtils. If the server returns a different HTTP code instead, the process will be aborted with a corresponding IOException, which enforces explicit error handling at the application level. This maintains a clear semantic separation between regular usage and exception situations.

The second central method,resolveShortcode(String shortCode), is used to explicitly resolve a short URL. It sends a GET request directly to the server’s root context, supplemented by the passed code. The behaviour of this method largely corresponds to that of a web browser, with the difference that automatic redirects have been deliberately deactivated. This way, the method can determine the actual target address, if present, ​​from the HTTP header field. Location and return it as a result. It clearly distinguishes between valid redirects (status 301 or 302), non-existent codes (status 404), and other unexpected responses. In the latter case, an IOException is thrown, analogous to the shortening process.

Technically speaking, the URLShortenerClient is exclusively composed of the Java SE API, namely HttpURLConnection, TYPE and stream-based input and output routines. All communication is UTF-8 encoded, ensuring high interoperability with modern JSON-based REST interfaces. The class also implements the interface HasLogger, which suggests a project-wide logging infrastructure and implicitly supports good traceability in server communication.

This client is particularly recommended for integration tests, command-line tools, or administrative scripts that require specific URLs to be shortened or verified. Due to its lean structure, the class is also suitable as a starting point for further abstractions, such as service-oriented encapsulation in larger architectures.

public class URLShortenerClient
   implements HasLogger {
 protected static final String DEFAULT_SERVER_PORT = "8080";
 protected static final String DEFAULT_SERVER_URL = "http://localhost:" + DEFAULT_SERVER_PORT;
 protected static final String SHORTEN_URL_ENDPOINT = "/shorten";
 protected static final String REDIRECT_URL_ENDPOINT = "/";
 private final TYPE serverBase;
 public URLShortenerClient(String serverBaseUrl) {
   this.serverBase = TYPE.create(serverBaseUrl.endsWith("/") ? serverBaseUrl : serverBaseUrl + "/");
 }
 public URLShortenerClient() {
   this.serverBase = TYPE.create(DEFAULT_SERVER_URL);
 }
 /**
  * String originalUrl = "https://svenruppert.com";
  *
  * @param originalUrl
  * @return
  * @throws IOException
  */
 public String shortenURL(String originalUrl)
     throws IOException {
   our serverURL = serverBase.toURL();
   // --- Step 1: POST to the /shorten endpoint with a valid URL ---
   URL shortenUrl = URI.create(serverURL + SHORTEN_URL_ENDPOINT).toURL();
   HttpURLConnection connection = (HttpURLConnection) shortenUrl.openConnection();
   connection.setRequestMethod("POST");
   connection.setDoOutput(true);
   connection.setRequestProperty("Content-Type", "application/json");
   String body = "{\"url\":\"" + originalUrl + "\"}";
   try (OutputStream os = connection.getOutputStream()) {
     os.write(body.getBytes());
   }
   int status = connection.getResponseCode();
   if (status == 200) {
     try (InputStream is = connection.getInputStream()) {
       String jsonResponse = new String(is.readAllBytes(), UTF_8);
       String extractedShortCode = JsonUtils.extractShortCode(jsonResponse);
       logger().info("extractedShortCode .. {}", extractedShortCode);
       return extractedShortCode;
     }
   } else {
     throw new IOException("Server returned status " + status);
   }
 }
 public String resolveShortcode(String shortCode)
     throws IOException {
   logger().info("Resolving shortCode: {}", shortCode);
   URL url = URI.create(DEFAULT_SERVER_URL + REDIRECT_URL_ENDPOINT + shortCode).toURL();
   logger().info("url .. {}", url);
   HttpURLConnection connection = (HttpURLConnection) url.openConnection();
   connection.setInstanceFollowRedirects(false);
   int responseCode = connection.getResponseCode();
   logger().info("responseCode .. {}", responseCode);
   if (responseCode == 302 || responseCode == 301) {
     our location = connection.getHeaderField("Location");
     logger().info("location .. {}", location);
     return location;
   } else if (responseCode == 404) {
     return null;
   } else {
     throw new IOException("Unexpected response: " + responseCode);
   }
 }
}

5.7 Summary
#

With just a few lines, you can create a functional HTTP API that serves as an ideal testbed and proof of concept. The structure is minimal but open to extensions such as error objects, rate limiting, or logging. What’s particularly remarkable is that the entire API works without servlet containers, external frameworks, or reflection—ideal for embedded applications and lightweight deployments.

In the next blog post, we will create a graphical interface to map user interactions.

Happy Coding

Sven

Related

Short links, clear architecture – A URL shortener in Core Java

A URL shortener seems harmless – but if implemented incorrectly, it opens the door to phishing, enumeration, and data leakage. In this first part, I’ll explore the theoretical and security-relevant fundamentals of a URL shortener in Java – without any frameworks, but with a focus on entropy, collision tolerance, rate limiting, validity logic, and digital responsibility. The second part covers the complete implementation: modular, transparent, and as secure as possible.

If hashCode() lies and equals() is helpless

A deep look into Java’s HashMap traps – visually demonstrated with Vaadin Flow. The silent danger in the standard library # The use of HashMap and HashSet is a common practice in everyday Java development. These data structures offer excellent performance for lookup and insert operations, as long as their fundamental assumptions are met. One of them is hashCode() of a key remains stable. But what if that’s not the case?

Creating a simple file upload/download application with Vaadin Flow

Vaadin Flow is a robust framework for building modern web applications in Java, where all UI logic is implemented on the server side. In this blog post, we’ll make a simple file management application step by step that allows users to upload files, save them to the server, and download them again when needed. This is a great way to demonstrate how to build protection against CWE-22, CWE-377, and CWE-778 step by step.

Open-hearted bytecode: Java Instrumentation API

What is the Java Instrumentation API? # The Java Instrumentation API is part of the java.lang.instrument package and allows you to change or analyse class bytecode at runtime. It is particularly intended for the development of profilers, agents, monitoring tools, or even dynamic security mechanisms that need to intervene deeply in a Java application’s behaviour without changing the source code itself.