Skip to main content

4 posts tagged with "kotlin"

View All Tags

[System Design Interview] Implementing a URL Shortener from Scratch

· 5 min read
Haril Song
Owner, Software Engineer at 42dot

banner

info

You can check the code on GitHub.

Overview

Shortening URLs started to prevent URLs from being fragmented in email or SMS transmissions. However, nowadays, it is more actively used for sharing specific links on social media platforms like Twitter or Instagram. It improves readability by not looking verbose and can also provide additional features such as collecting user statistics before redirecting to the URL.

In this article, we will implement a URL shortener from scratch and explore how it works.

What is a URL Shortener?

Let's first take a look at the result.

You can run the URL shortener we will implement in this article directly with the following command:

docker run -d -p 8080:8080 songkg7/url-shortener

Here is how to use it. Simply input the long URL you want to shorten as the value of longUrl.

curl -X POST --location "http://localhost:8080/api/v1/shorten" \
-H "Content-Type: application/json" \
-d "{
\"longUrl\": \"https://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8\"
}"
# You will receive a random value like tN47tML.

Now, if you access http://localhost:8080/tN47tML in your web browser,

image

You will see that it correctly redirects to the original URL.

Before Shortening

After Shortening

Now, let's see how we can shorten URLs.

Rough Design

Shortening URLs

  1. Generate an ID before storing the longUrl.
  2. Encode the ID to base62 to create the shortUrl.
  3. Store the ID, shortUrl, and longUrl in the database.

Memory is finite and relatively expensive. RDB can be quickly queried through indexes and is relatively cheaper compared to memory, so we will use RDB to manage URLs.

To manage URLs, we first need to secure an ID generation strategy. There are various methods for ID generation, but it may be too lengthy to cover here, so we will skip it. I will simply use the current timestamp for ID generation.

Base62 Conversion

By using ULID, you can generate a unique ID that includes a timestamp.

val id: Long = Ulid.fast().time // e.g., 3145144998701, used as a primary key

Converting this number to base62, we get the following string.

tN47tML

This string is stored in the database as the shortUrl.

idshortlong
3145144998701tN47tMLhttps://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8

The retrieval process will proceed as follows:

  1. A GET request is made to localhost:8080/tN47tML.
  2. Decode tN47tML from base62.
  3. Obtain the primary key 3145144998701 and query the database.
  4. Redirect the request to the longUrl.

Now that we have briefly looked at it, let's implement it and delve into more details.

Implementation

Just like the previous article on Consistent Hashing, we will implement it ourselves. Fortunately, implementing a URL shortener is not that difficult.

Model

First, we implement the model to receive requests from users. We simplified the structure to only receive the URL to be shortened.

data class ShortenRequest(
val longUrl: String
)

We implement a Controller to handle POST requests.

@PostMapping("/api/v1/shorten")
fun shorten(@RequestBody request: ShortenRequest): ResponseEntity<ShortenResponse> {
val url = urlShortenService.shorten(request.longUrl)
return ResponseEntity.ok(ShortenResponse(url))
}

Base62 Conversion

Finally, the most crucial part. After generating an ID, we encode it to base62 to shorten it. This shortened string becomes the shortUrl. Conversely, we decode the shortUrl to find the ID and use it to query the database to retrieve the longUrl.

private const val BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

class Base62Conversion : Conversion {
override fun encode(input: Long): String {
val sb = StringBuilder()
var num = BigInteger.valueOf(input)
while (num > BigInteger.ZERO) {
val remainder = num % BigInteger.valueOf(62)
sb.append(BASE62[remainder.toInt()])
num /= BigInteger.valueOf(62)
}
return sb.reverse().toString()
}

override fun decode(input: String): Long {
var num = BigInteger.ZERO
for (c in input) {
num *= BigInteger.valueOf(62)
num += BigInteger.valueOf(BASE62.indexOf(c).toLong())
}
return num.toLong()

}
}

The length of the shortened URL is inversely proportional to the size of the ID number. The smaller the generated ID number, the shorter the URL can be made.

If you want the length of the shortened URL to not exceed 8 characters, you should ensure that the size of the ID does not exceed 62^8. Therefore, how you generate the ID is also crucial. As mentioned earlier, to simplify the content in this article, we handled this part using a timestamp value.

Test

Let's send a POST request with curl to shorten a random URL.

curl -X POST --location "http://localhost:8080/api/v1/shorten" \
-H "Content-Type: application/json" \
-d "{
\"longUrl\": \"https://www.google.com/search?q=url+shortener&sourceid=chrome&ie=UTF-8\"
}"

You can confirm that it correctly redirects by accessing http://localhost:8080/{shortUrl}.

Conclusion

Here are some areas for improvement:

  • By controlling the ID generation strategy more precisely, you can further shorten the shortUrl.
    • If there is heavy traffic, you must consider issues related to concurrency.
    • Snowflake
  • Using DNS for the host part can further shorten the URL.
  • Applying cache to the Persistence Layer can achieve faster responses.

[Kotlin] Infix Functions

· One min read
Haril Song
Owner, Software Engineer at 42dot

In Kotlin, there is a method of defining functions called Infix functions, which is a syntax that was unimaginable while using Java as the primary language. Let's introduce this for those who are starting with Kotlin.

Member functions with a single parameter can be converted into Infix functions.

One of the prominent examples of Infix functions is the to function included in the standard library.

val pair = "Ferrari" to "Katrina"
println(pair)
// (Ferrari, Katrina)

You can define new Infix functions like to as needed. For example, you can extend Int as follows:

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "Hello ")
// Hello Hello

If you want to redefine to as a new Infix function called onto, you can write it as follows:

infix fun String.onto(other: String) = Pair(this, other)
val myPair = "McLaren" onto "Lucas"
println(myPair)
// (McLaren, Lucas)

Such Kotlin syntax enables quite unconventional coding methods.

class Person(val name: String) {
val likedPeople = mutableListOf<Person>()

infix fun likes(other: Person) {
likedPeople.add(other)
}
}

fun main() {
val sophia = Person("Sophia")
val claudia = Person("Claudia")

sophia likes claudia // !!
}

Reference

Kotlin Docs

[Kotlin] Enhanced Loops

· 2 min read
Haril Song
Owner, Software Engineer at 42dot

In Kotlin, you can write much simpler and more convenient loops compared to Java. Let's see how you can use them.

1. .. operator

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using .. creates a traditional loop that increments by 1.

2. downTo

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.size - 1 downTo 0) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using downTo creates a loop that decrements as expected.

3. step

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0..fruits.size - 1 step 2) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

With the step keyword, you can implement a loop that skips a specific number of elements. This also applies to downTo.

4. until

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 until fruits.size) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Using until creates a loop that does not include the last number, eliminating the need for -1.

5. lastIndex

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in 0 .. fruits.lastIndex) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

Now, using the lastIndex property, loops start to become easier to read. But of course, there's more to explore.

6. indices

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for (index in fruits.indices) {
val fruit = fruits[index]
println("$index: $fruit")
}
}

indices returns the index range of the collection.

7. withIndex()

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
}

By using withIndex(), extracting both index and value simultaneously simplifies the code, resembling Python's simplicity. This should be sufficient for most loop scenarios, but there's one more method left.

8. forEachIndexed

val fruits = listOf("Apple", "Banana", "Cherry", "Durian")

fun main() {
fruits.forEachIndexed { index, fruit ->
println("$index: $fruit")
}
}

Using a lambda function with forEachIndexed can make the code more concise, intuitive, and straightforward. Choose the appropriate method that suits your needs.


Reference

Kotlin Tips: Loops

Utilizing Ellipsoids on Earth with Kotlin

· 3 min read
Haril Song
Owner, Software Engineer at 42dot

Background

earth image reference1

Considering that the Earth is neither flat nor a perfect sphere, but an irregular ellipsoid, there is no perfect formula for quickly and accurately calculating the distance between two points at different longitudes and latitudes.

However, by using the geotools library, you can obtain mathematically corrected approximations quite easily.

Adding Dependencies

To use the Earth ellipsoid in geotools, you need to add the relevant library dependencies.

repositories {
maven { url "https://repo.osgeo.org/repository/release/" }
maven { url "https://download.osgeo.org/webdav/geotools/" }
mavenCentral()
}

dependencies {
...
implementation 'org.geotools:gt-referencing:26.2'
...
}

Writing Code

First, define the coordinates of Seoul and Busan as an enum class.

enum class City(val latitude: Double, val longitude: Double) {
SEOUL(37.5642135, 127.0016985),
BUSAN(35.1104, 129.0431);
}

Next, let's look at a simple usage example through a test code.

class EllipsoidTest {

@Test
internal fun createEllipsoid() {
val ellipsoid = DefaultEllipsoid.WGS84 // Creates an ellipsoid that is as close to the Earth as possible using the WGS84 geodetic system used in GPS

val isSphere = ellipsoid.isSphere // Determines if it is a sphere or an ellipsoid
val semiMajorAxis = ellipsoid.semiMajorAxis // Equatorial radius, the longer radius of the ellipsoid
val semiMinorAxis = ellipsoid.semiMinorAxis // Polar radius, the shorter radius of the ellipsoid
val eccentricity = ellipsoid.eccentricity // Eccentricity, indicates how close the ellipsoid is to a sphere
val inverseFlattening = ellipsoid.inverseFlattening // Inverse flattening value
val ivfDefinitive = ellipsoid.isIvfDefinitive // Indicates if the inverse flattening is definitive for this ellipsoid

// Orthodromic distance
val orthodromicDistance = ellipsoid.orthodromicDistance(
City.SEOUL.longitude,
City.SEOUL.latitude,
City.BUSAN.longitude,
City.BUSAN.latitude
)

println("isSphere = $isSphere")
println("semiMajorAxis = $semiMajorAxis")
println("semiMinorAxis = $semiMinorAxis")
println("eccentricity = $eccentricity")
println("inverseFlattening = $inverseFlattening")
println("ivfDefinitive = $ivfDefinitive")
println("orthodromicDistance = $orthodromicDistance")
}
}
isSphere = false
semiMajorAxis = 6378137.0
semiMinorAxis = 6356752.314245179
eccentricity = 0.08181919084262128
inverseFlattening = 298.257223563
ivfDefinitive = true
orthodromicDistance = 328199.9794919944

You can create an Earth ellipsoid with DefaultEllipsoid.WGS84. If you use SPHERE instead of WGS84, a sphere with a radius of 6371km will be created.

The distance result is in meters (m), so converting it to kilometers shows approximately 328km. If you search on Google, you may find 325km, so considering that there may be differences between the coordinates I chose and those chosen by Google, this is not a bad figure.

There are many other functions available as well. However, covering them all in this post would be too extensive, so if needed, I will cover them in another post.

info

The margin of error may not be satisfactory depending on business requirements, so before actual implementation, make sure to thoroughly test other methods in geotools.


Footnotes

  1. An Overview of SRID and Coordinate System