Observability in Action Part 1: Enhancing Your Codebase with OpenTelemetry

Observability in Action Part 1: Enhancing Your Codebase with OpenTelemetry

A Step-by-Step Guide to Building a Kotlin Client Library for Cat Facts API

TL;DR: In this article, you'll learn how to build a client library for fetching cat facts from an API using Kotlin. The library can handle multiple concurrent API calls and return unique facts. OpenTelemetry and service instrumentation will be covered in later parts of this series to enhance observability.


In this series, we'll delve into the steps for adding observability to your codebase. Initially, we'll develop a library that retrieves data from a remote API. Following that, we'll construct a service using this library to fetch and save this data in a database.

As we progress, we'll infuse observability into the service, demonstrating their behavior in an environment resembling production. Once the service is equipped with instrumentation, we'll introduce a filter to it that dismisses overly large requests. This filter, too, will be instrumented.

To wrap up, we'll integrate an OpenTelemetry collector into our service. This will gather all traces and metrics, transmitting them to an external location to mitigate any service overhead.

Series Outline

All code examples for this series are available on GitHub:

kitten poking cat's nose

Introduction to Service Instrumentation

What is service instrumentation? And why do I need it?

Service instrumentation is the process of collecting data from different components in your system (e.g. services) to benefit insights into the system's performance, behavior, and usage. This data can be used to optimize the system, troubleshoot, and improve the user experience.

More specifically, we will use OpenTelemetry. OpenTelemetry documentation states:

OpenTelemetry, also known as OTel for short, is a vendor-neutral open-source Observability framework for instrumenting, generating, collecting, and exporting telemetry data such as traces, metrics, logs. As an industry-standard, it is natively supported by a number of vendors.

That means that OpenTelemetry is a framework that allows you to easily add instrumentation to your codebase and collect the data in a vendor-agnostic way. It supports multiple programming languages and provides a unified API for collecting and exporting telemetry data to various backends.

OpenTelemetry also provides a set of libraries and integrations that make it easy to instrument popular frameworks, libraries, and services. With OpenTelemetry, developers can easily add telemetry to their services and gain visibility into their systems' performance and behavior.

For more information about observability, check out a great article "How Observability Changed My (Developer) Life" written by a colleague of mine, Mariusz Sołtysiak.

Cat in helmet

Building the Client Library

We'll kick off by developing our client library named cat-fact-client. This library will fetch cat facts from the Cat Facts API.

At its core, our library is straightforward. It endeavors to fetch a specified number of facts. While the API restricts fact selection, we compensate by invoking the API multiple times, as required, making the best effort to serve the requested number of facts.

Our library will utilize:

  • Kotlin - The crux of our library, it will be scripted in Kotlin using coroutines.

  • Gradle - Our trusted build system and dependency manager.

  • Retrofit - Our choice for an HTTP client.

  • Jackson - Essential for serialization, particularly as we’ll be integrating with Spring Boot which defaults to Jackson.

Let’s get coding!

Cat writing code

Adding Dependencies

Kick-off by adding the essential dependencies to the build.gradle.kts file:

dependencies {
    // Coroutines

    // Serialization

    // Retrofit

Domain Modeling

When you ping the Cat Facts API, expect a response similar to:

{"fact":"Cats have \"nine lives\" thanks to a flexible spine and powerful leg and back muscles","length":83}

Our primary concern is the fact field. To determine the fact length, we simply utilize fact.length. This gives rise to our model:

data class Fact(val value: String)

By leveraging Kotlin's value class, we optimize resource utilization. While we interact solely with Fact objects, these objects are substituted with String objects during compilation.

Thus, we revised our code to:

value class Fact(val value: String)

Constructing the HTTP Client

Having established our domain model, it's time to construct an HTTP client for API calls.

This would be our client's blueprint:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import retrofit2.http.GET

internal interface CatFactClient {
    suspend fun fact(): CatFactResponse

@JsonIgnoreProperties(ignoreUnknown = true)
internal data class CatFactResponse(
    val fact: String,

You’ll observe a solitary function, fact(), geared towards API communication, yielding a CatFactResponse. We’ve intentionally omitted the length field, as highlighted earlier.

Connecting everything together

With foundational pieces in place, let's merge them to manifest our core library logic.

Commence by configuring an instance of the HTTP client:

private const val API_BASE_URL = "https://catfact.ninja/"

private var client = Retrofit.Builder()

Now, our business logic:

override suspend fun get(numberOfFacts: Int): Set<Fact> =
    coroutineScope {
        (1..numberOfFacts).map {
            async { client.fact() }
            .map { Fact(it.fact) }

This function concurrently dispatches numberOfFacts calls to the API, awaits all replies, translates them into the domain model, and returns a fact set. We utilize Set over List since the API doesn't assure unique responses.

Inspect the finalized version of the code here.

Happy Cat

This piece isn’t tailored to guide library publishing. However, if you’re inclined, relevant settings can be found here.

Our library's artifact, version 0.1.0, is available on GitHub packages and awaits your exploration. An updated version (0.2.0) offers mock implementations, bypassing internet prerequisites with a few breaking changes. Nevertheless, the core remains unaltered.


This article provides a comprehensive guide on building a client library for fetching cat facts from an API, using Kotlin, Gradle, Retrofit, and Jackson. The library is designed to handle multiple concurrent API calls and return a set of unique facts. The implementation of OpenTelemetry and service instrumentation is planned for the next parts of this series, ultimately enhancing the observability of the service.


  • Mariusz Sołtysiak - for moral support, review, and suggestions while writing this series of articles.

Did you find this article valuable?

Support Yonatan Karp-Rudin by becoming a sponsor. Any amount is appreciated!