← thecodex.expert · The Codex Family of Knowledge
Tier 2 · Intermediate · Kotlin Project

URL Shortener

Turn long URLs into short codes and back again, saved to a file. Learn two-way lookups and generating unique keys.

🧠 Teaches how to think spoonfed, every age Last verified:

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.

Where this shows up: bit.ly and every link shortener, QR-code targets, affiliate links, any system that maps a short key to a longer value — which includes caches, session stores, and lookup services generally.

2 How to Think About It

Think about the mapping, before any code:

The plan — in plain English
1. Keep a store mapping short code → long URL. → 2. To shorten: make a new random short code, save the mapping, return the code. → 3. To expand: look the code up and return the URL. → 4. Save the store to a file so links survive.

Long URL comes in

Generate a short code

Save code to URL mapping

Return the short code

Short code comes in

Look up the URL

Return the long URL

3 The Build — explained part by part

Here is the complete shortener. Read each part’s note below — you should understand the whole thing from the notes alone.

Kotlinshortener.kt
import java.io.File
import java.security.SecureRandom

private val FILE = File("links.txt")
private val CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
private val secureRandom = SecureRandom()

fun load(): MutableMap<String, String> {
    if (!FILE.exists()) return mutableMapOf()
    val links = mutableMapOf<String, String>()
    for (line in FILE.readLines()) {
        if (line.isBlank()) continue
        val (code, url) = line.split("|", limit = 2)
        links[code] = url
    }
    return links
}

fun save(links: Map<String, String>) {
    FILE.writeText(links.entries.joinToString("\n") { "${it.key}|${it.value}" })
}

/** A random short code from letters and digits. */
fun makeCode(length: Int = 6): String {
    val sb = StringBuilder(length)
    repeat(length) { sb.append(CHARS[secureRandom.nextInt(CHARS.length)]) }
    return sb.toString()
}

fun shorten(links: MutableMap<String, String>, url: String): String {
    var code = makeCode()
    while (links.containsKey(code)) { // avoid a clash with an existing code
        code = makeCode()
    }
    links[code] = url
    return code
}

/** Returns null if the code is unknown. */
fun expand(links: Map<String, String>, code: String): String? = links[code]

fun main() {
    val links = load()
    val code = shorten(links, "https://example.com/a/very/long/link")
    save(links)
    println("Short code: $code")
    println("Expands to: ${expand(links, code)}")
}
⚠ No in-browser playground here
Kotlin compiles to real JVM bytecode, not something a browser can run directly — running it live would need either a server-side compiler or a third-party embed, the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with a real kotlinc on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
fun makeCode(length: Int = 6): String — a default parameter value: calling makeCode() with no arguments uses 6, but any caller (including the tests) can pass a different length explicitly — no overload required, unlike Java.

java.security.SecureRandom — the same secure generator used by the password generator project; a short code is effectively a small secret, so the same reasoning applies.

while (links.containsKey(code)) { code = makeCode() } — regenerates on the rare chance of a collision with an existing code, rather than ever overwriting someone else's shortened link.

fun expand(links: Map<String, String>, code: String): String? — returns the nullable String? that Map.get already returns for a missing key, rather than throwing or returning a confusing sentinel value — the caller decides what “not found” should mean.
Common mistakes — and how to avoid them
✗ Generating a short code and saving it without checking for a collision.
✓ With enough links stored, two different URLs can eventually generate the same random code. Check links.containsKey(code) and retry, as this program does, rather than silently overwriting an existing link.
✗ Using links[code]!! to force-unwrap the lookup instead of handling a missing code.
✓ !! throws a NullPointerException the moment a code does not exist — exactly the crash null-safety exists to prevent. Return the nullable result (as expand does) and let the caller decide, e.g. with ?: or an if.
✗ Splitting a saved "code|url" line with no limit, breaking on a URL that itself happens to contain a |.
✓ line.split("|", limit = 2) only splits on the first |, leaving the rest of the line (the full URL) intact.

4 Test & Prove Each Part

How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.

Shortening then expanding returns the original URL
The default code length is 6
A custom length is respected
An unknown code expands to null, not a crash
Every generated character comes from the allowed pool
Kotlinshortener_test.kt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

class ShortenerTest {
    @Test
    fun shortenThenExpandRoundTrips() {
        val links = mutableMapOf<String, String>()
        val code = shorten(links, "https://example.com")
        assertEquals("https://example.com", expand(links, code))
    }

    @Test
    fun defaultCodeLengthIsSix() {
        assertEquals(6, makeCode().length)
    }

    @Test
    fun customCodeLength() {
        assertEquals(10, makeCode(10).length)
    }

    @Test
    fun unknownCodeExpandsToNull() {
        assertNull(expand(mutableMapOf(), "nope"))
    }

    @Test
    fun codeComesFromAllowedChars() {
        assertTrue(Regex("[A-Za-z0-9]+").matches(makeCode(200)))
    }
}

Compile with kotlinc shortener.kt shortener_test.kt -include-runtime -d shortener.jar and run with JUnit's own runner. shorten/expand operate on a plain MutableMap<String, String>, so the tests never touch links.txt at all.

5 The Interface

INPUTSHORTENlong url
What it expects
shorten(links, "https://example.com/very/long")
OUTPUTEXPANDshort code
What it returns
Short code: T0Gl9y
Expands to: https://example.com/very/long

6 Run It & Automate It

Save the code as shortener.kt and compile it with kotlinc shortener.kt -include-runtime -d shortener.jar.

Run it locally
kotlinc shortener.kt -include-runtime -d shortener.jar && java -jar shortener.jar
Shortens one hard-coded URL and immediately expands the code it generated, to prove the round trip works.

A CI tool like Jenkins compiles and tests automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
Short code: pLGT4r
Expands to: https://example.com/a/very/long/link
If it breaks — how to fix it
🚨 Expands to: null
The code being looked up does not match any saved entry — make sure save(links) ran after shorten added the new code, and that both calls are using the same in-memory map.
🚨 The saved links.txt file is empty after running
Check that save(links) is actually called before the program exits; a program that only ever calls load() and shorten() without save() never persists anything.
GroovyJenkinsfile
// Jenkinsfile &mdash; compiles and tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Kotlin') {
            steps {
                sh 'kotlinc -version'                             // confirm the compiler is installed
            }
        }
        stage('Compile and test') {
            steps {
                sh 'kotlinc shortener.kt shortener_test.kt -include-runtime -d build.jar'  // one real JVM jar, no build tool required
                sh 'java -cp build.jar:kotlin-test-junit.jar:junit.jar org.junit.runner.JUnitCore ShortenerTest'
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working url shortener. Extend it:

  1. Accept a real URL on the command line. Replace the hard-coded example.com URL with args[0]. (Teaches: main(args: Array<String>).)
  2. Serve it over HTTP. Combine this with the REST API project so a real browser redirect works. (Teaches: combining two projects on this page.)
  3. Expire old links. Store a timestamp alongside each URL and prune anything too old. (Teaches: java.time.Instant.)
  4. Reject invalid URLs. Validate the input with java.net.URI before shortening it. (Teaches: catching a real checked-adjacent exception from the JDK.)
What you learned
You learned Kotlin’s default parameter values (no overloads needed), returning a nullable type instead of throwing or force-unwrapping with !!, and the same SecureRandom discipline from the password generator applied to a different kind of secret. Related reference: Null Safety & Types, Functions in Kotlin.