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
Think about the mapping, before any code:
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.
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)}")
}kotlinc on your own machine instead; the “Run It” section explains exactly how.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.
links.containsKey(code) and retry, as this program does, rather than silently overwriting an existing link.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."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.
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
What it expects
shorten(links, "https://example.com/very/long")What it returns
Short code: T0Gl9y
Expands to: https://example.com/very/long6 Run It & Automate It
Save the code as shortener.kt and compile it with kotlinc shortener.kt -include-runtime -d shortener.jar.
kotlinc shortener.kt -include-runtime -d shortener.jar && java -jar shortener.jarShortens 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.
Short code: pLGT4r
Expands to: https://example.com/a/very/long/linkExpands to: nullsave(links) ran after shorten added the new code, and that both calls are using the same in-memory map.links.txt file is empty after runningsave(links) is actually called before the program exits; a program that only ever calls load() and shorten() without save() never persists anything.// Jenkinsfile — 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 — look above.' }
}
}
You have a working url shortener. Extend it:
- Accept a real URL on the command line. Replace the hard-coded
example.comURL withargs[0]. (Teaches:main(args: Array<String>).) - Serve it over HTTP. Combine this with the REST API project so a real browser redirect works. (Teaches: combining two projects on this page.)
- Expire old links. Store a timestamp alongside each URL and prune anything too old. (Teaches:
java.time.Instant.) - Reject invalid URLs. Validate the input with
java.net.URIbefore shortening it. (Teaches: catching a real checked-adjacent exception from the JDK.)
!!, and the same SecureRandom discipline from the password generator applied to a different kind of secret. Related reference: Null Safety & Types, Functions in Kotlin.