System Design: Build URL Shortening Service using Ktor (Part-2)

System Design: Build URL Shortening Service using Ktor (Part-2)

In this blog series, we are going to learn about the most common question asked in System design rounds. We will also implement the same using Ktor.

Welcome to part - 2 of the system design series of building your own URL Shortening Service. You can check Part: 1 here,

Till now, you have built your service and database. In this part, you will build,

  • Exception handling
  • Repository layer
  • Domain layer

Let us start before any delay.

Setting up Exception Handling

If you remember you have Status selected as a feature for this project. Status is responsible to handle any exception.

First, create a file called Status in,

base -> Status

This will have an extension function configureStatusPages and will look like,

fun Application.configureStatusPages() {
    install(StatusPages) {
        exception<NotFoundException> { cause ->
            call.respond(ExceptionResponse(HttpStatusCode.NotFound, cause.message.toString()))
        }
        exception<SomethingWentWrongException> { cause ->
            call.respond(ExceptionResponse(HttpStatusCode.InternalServerError, cause.message.toString()))
        }
    }
}

class NotFoundException(message: String?) : RuntimeException(message)

class SomethingWentWrongException(message: String = "Something went wrong!") : RuntimeException(message)

data class ExceptionResponse(val code: HttpStatusCode, val message: String? = null)

Here, create two custom Exception classes and the ExceptionResponse class to return as output. Then install the StatusPages and inside the block, which will handle both our custom exceptions.

This basically means if you throw any of these exceptions the above responses will be triggered of type ExceptionResponse.

Now, you have your StatusPages successfully setup. As a next step, you need to create its provider same as how you did for UrlService in the following package,

base -> provider -> exception

In the exception package, create two files named ExceptionProvider and ExceptionProviderImpl`.

The ExceptionProvider interface looks like,

interface ExceptionProvider {

    fun respondWithNotFoundException(message: String?): Exception

    fun respondWithGenericException(message: String?): Exception

    fun respondWithSomethingWentWrongException(): Exception
}

Lastly, the implementation class, ExceptionProviderImpl will look like,

class ExceptionProviderImpl : ExceptionProvider {

    override fun respondWithNotFoundException(message: String?): Exception {
        return NotFoundException(message)
    }

    override fun respondWithGenericException(message: String?): Exception {
        return Exception(message)
    }

    override fun respondWithSomethingWentWrongException(): Exception {
        return SomethingWentWrongException()
    }
}

ExceptionProvider will be responsible for throwing errors in the repository layer.

As a last step, you need to create is Locator in di package called ExceptionLocator. This locator will look like,

object ExceptionLocator {

    fun provideExceptionProvider(): ExceptionProvider {
        return ExceptionProviderImpl()
    }
}

You are done setting up everything you need for exception handling.

Now, let us setup our repository layer.

Setting up Repository Layer

First, create a repository package like,

feature -> url -> repository

Now, inside this as well you will create two files UrlRepository and UrlRepositoryImpl same as the service package.

Repository layer is like a link between Service and Domain layer.

UrlRepository is an interface which looks like,

interface UrlRepository {

    suspend fun createShortUrl(originUrl: String): BaseResponse<Any>

    suspend fun findOriginalUrl(shortUrl: String): String?

    suspend fun getTotalCount(shortUrl: String): BaseResponse<Any>
}

Here, you can see we have BaseResponse as return type which is present in,

util -> Response

and it looks like,

interface BaseResponse<T : Any>

data class SuccessResponse<T : Any>(
    val statusCode: HttpStatusCode,
    val data: T? = null,
) : BaseResponse<T>

data class UnSuccessResponse<T : Any>(
    val statusCode: HttpStatusCode,
    val exception: T? = null,
) : BaseResponse<T>

Here, the two data classes are responsible for handling success and failure states. They both are of BaseResponse type.

These functions are responsible for having the execution and checks before hitting the database layer.

Now, implement the UrlRepository to UrlRepositoryImpl class like same as how you did with UrlService interface.

UrlRepositoryImpl class now looks like,

class UrlRepositoryImpl(
    private val urlService: UrlService,
    private val exceptionProvider: ExceptionProvider
) : UrlRepository {

    override suspend fun createShortUrl(originUrl: String): BaseResponse<Any> {
        val url = urlService.findShortUrl(originUrl)
        return if (url != null) {
            SuccessResponse(HttpStatusCode.Found, url)
        } else {
            val newShortUrl = urlService.createShortUrl(originUrl)
            SuccessResponse(HttpStatusCode.Created, newShortUrl)
        }
    }

    override suspend fun findOriginalUrl(shortUrl: String): String? {
        return urlService.findOriginalUrl(shortUrl)
    }

    override suspend fun getTotalCount(shortUrl: String): BaseResponse<Any> {
        val count = urlService.getTotalCount(shortUrl)
        if (count != null) {
            return SuccessResponse(HttpStatusCode.OK, count)
        } else {
            throw exceptionProvider.respondWithNotFoundException("Url not found!")
        }
    }
}

Here, you can see our class takes two parameters i.e. our UrlService interface and ExceptionProvider interface to follow the principle of passing the dependency from outside.

In the createShortUrl, take the original/long URL and first check in the database if it's present or not using findShortUrl.

If it's present you just return SuccessResponse with Found HTTP Code or create a new short URL of that original URL and return SuccessResponse with Created HTTP Code.

All of these functions are exposed via the UrlService interface and as mentioned in the previous blog, you are not exposing the implementation of the Service but only the methods/functions.

In the getTotalCount function, you want to check the total number of times the short URL was used by the users you can first check if the short URL is present or not. If it's present then do our operation as normal but when you don't find it, just throw an exception using the ExceptionProvider interface.

The implementation of the UrlRepository is done. But something is left 🤭

Guessed it right!!! The DI part is left.

For that, first create a package in,


base -> provider -> repository

And create two files, RepositoryProvider and RepositoryProviderImpl.

The ReposityProvider will hold the method of our UrlRepository like,

interface RepositoryProvider {

    fun provideUrlRepository(): UrlRepository
}

RepositoryProviderImpl will now implement the RepositoryProvider interface like,

class RepositoryProviderImpl(private val serviceLocator: ServiceLocator) : RepositoryProvider {

    override fun provideUrlRepository(): UrlRepository {

    }
}

If you look closely, you have ServiceLocator as a constructor parameter for our RepositoryProviderImpl class.

This Service locator will be responsible for passing the UrlServce to the UrlRepositoryImpl class from outside.

Now, the complete code looks like,

class RepositoryProviderImpl(private val serviceLocator: ServiceLocator) : RepositoryProvider {

    override fun provideUrlRepository(): UrlRepository {
        return RepositoryLocator.provideUrlRepository(
            serviceLocator.provideUrlService()
        )
    }
}

Here, the repository implementation is coming from the RepositoryLocator. Now you need to create the RepositoryLocator which will be present in,

di -> RepositoryLocator

The RepositoryLocator looks like,

object RepositoryLocator {

    fun provideUrlRepository(urlService: UrlService): UrlRepository {
        return UrlRepositoryImpl(urlService,ExceptionLocator.provideExceptionProvider())
    }

    fun provideRepositoryProvider(): RepositoryProvider {
        return RepositoryProviderImpl(ServiceLocator)
    }
}

Here, provideUrlRepository returns the instance of UrlRepository and provideRepositoryProvider returns the instance of RepositoryProvider. These functions are responsible for providing the Repository wherever required.

Now, you have your Repository setup as well.

Lastly, the layer left is the domain layer. Once this is done, you can just plug the routes and things will start coming out nicely.

Setting up the domain layer.

Domain layer in any project is platform-independent and is only language-based.

In your case, the Domain layer is JVM-based and will be exposed to the routes and will be responsible for taking requests and responding as the response.

In the UrlRepository, you have 3 functions. So, for the domain layer, you will create the UseCases. But first, create a BaseUseCase which will be extended by all the use cases. Create a BaseUse case in,

base -> BaseUseCase

and it looks like,

/** A Use Case that takes an argument and returns a result. */
interface BaseUseCase<in I, R : Any> {
    /** Executes this use case with given input. */
    suspend operator fun invoke(input: I): BaseResponse<R>
}

This interface, will take an input and return an output or type BaseResponse. You are going to extend this to all the use cases where possible.

Now, as a next step create a package in,

feature -> url -> domain

And as the first UseCase, create a file CreateShortUrlUseCase and extend it using BaseUseCase. This use case will take a URL as an input and will return BaseResponse<Any>.

Also, pass the UrlRepository as the constructor parameter because they will be responsible for passing the methods to the UseCase.

The CreateShortUrlUseCase looks like,

class CreateShortUrlUseCase(private val urlRepository: UrlRepository) : BaseUseCase<String, Any> {
    /** Executes this use case with given input. */
    override suspend fun invoke(input: String): BaseResponse<Any> {
        return urlRepository.createShortUrl(input)
    }
}

Here, the return statement is passing the createShortUrl function from the repository as it was of BaseReponse<Any> return type as well.

Similarly, create FindUrlHitCountUseCase in the same package. The implementation you will see would be the same as the above with only the return function being changed from the repository. FindUrlHitCountUseCase looks like,

class FindUrlHitCountUseCase(private val urlRepository: UrlRepository):BaseUseCase<String,Any> {
    /** Executes this use case with given input. */
    override suspend fun invoke(input: String): BaseResponse<Any> {
        return urlRepository.getTotalCount(input)
    }
}

Now, as you have 3 functions in your repository and you created two use cases. You are left with one, i.e.

    suspend fun findOriginalUrl(shortUrl: String): String?

You can see here, it doesn't have BaseResponse as return type but String as return value. Now, create a use case for this that will be called FindShortUrlUseCase.

This use case will not extend BaseUseCase as the return type is not BaseResponse.

FindShortUrlUseCase looks like,

class FindShortUrlUseCase(private val urlRepository: UrlRepository) {
    /** Executes this use case with given input. */
    suspend fun invoke(input: String): String? {
        return urlRepository.findOriginalUrl(input)
    }
}

You can see here, you don't have BaseUseCase implemented so this works as a normal class.

Now, we are done implementing all our use cases.

But one last thing is left. Think of it what is left 👻.

Correct!!! the provides and locator.

First, create a DomainProvider and DomainProviderImpl in,

base -> provider -> domain

The DomainProvider will have all the provider functions of the use cases. You have three use cases in your project so, you would have 3 functions. DomainProvider looks like,

interface DomainProvider {

    fun provideCreateShortUrlUseCase(): CreateShortUrlUseCase

    fun provideFindShortUrlUseCase(): FindShortUrlUseCase

    fun provideFindUrlHitCountUseCase(): FindUrlHitCountUseCase
}

Now, you also need needs it implementation i.e. DomainProviderImpl which looks like,

class DomainProviderImpl(private val repositoryProvider: RepositoryProvider) : DomainProvider {

    override fun provideCreateShortUrlUseCase(): CreateShortUrlUseCase {
        return CreateShortUrlUseCase(repositoryProvider.provideUrlRepository())
    }

    override fun provideFindShortUrlUseCase(): FindShortUrlUseCase {
        return FindShortUrlUseCase(repositoryProvider.provideUrlRepository())
    }
    override fun provideFindUrlHitCountUseCase(): FindUrlHitCountUseCase {
        return FindUrlHitCountUseCase(repositoryProvider.provideUrlRepository())
    }
}

Here, the DomainProviderImpl takes RepositoryProvider as a constructor parameter and will be responsible for passing the repositories to the use cases instances like shown above.

Lastly, only the DomainLocator is left which will be present in,

di -> DomainLocator

DomainLocator will be exposing the DomainProvider from a function but will have DomainProviderImpl in the return value.

The DomainLocator looks like,

object DomainLocator {

    fun provideDomainProvider(): DomainProvider {
        return DomainProviderImpl(
            RepositoryLocator.provideRepositoryProvider()
        )
    }
}

Now we have all our Locator objects setup for Database, Domain, Service, Repository, and Exception.

This is all for the domain setup and this blog.

In part -3 of the series, we will setup our routing using locations and utilize the DomainLocator to provide the specific use cases to the routes.

If you want to read more about the mongo operations check this blog.

That is all for Part 2 of this blog. Click here for Part -3

If you like the series, do share it with people :)

Check the code here.

Thank you for reading :) Hope you learned something from my experience.

If you have anything more to discuss, I will be happy to get connected at hi_man_shoe.

See you in the next blog :)

Did you find this article valuable?

Support Himanshu Singh by becoming a sponsor. Any amount is appreciated!