How to retrieve Geolocation Information from an IP Address in Kotlin + Spring Boot

How to retrieve Geolocation Information from an IP Address in Kotlin + Spring Boot

Overview

  • In the operation of production-level backend services, there often arises a need to extract geographical information based on request IP addresses, whether for business purposes or for monitoring. There are several approaches to address this requirement, but this article focuses on a method to safely and quickly obtain Geolocation information using a local database file without any restrictions.

Downloading the GeoLite2 Local Database

  • MaxMind, founded in 2002, is a specialized company that has been dedicated to the field of IP intelligence for over 20 years. We will use their free GeoLite2 IP Geolocation local database file to create our examples. You can download it for free after registering and logging in on the company's website. [Download Link]
  • As an alternative download method, a general user named P3TERX has been providing the latest version of the database in his GitHub repository for years, enabling download without needing to sign up or log in to the MaxMind website. [GitHub Repository Link]
$ wget -nv -O GeoLite2-ASN.mmdb https://git.io/GeoLite2-ASN.mmdb
$ wget -nv -O GeoLite2-City.mmdb https://git.io/GeoLite2-City.mmdb
$ wget -nv -O GeoLite2-Country.mmdb https://git.io/GeoLite2-Country.mmdb
  • The database consists of three files and new versions are uploaded to the website every two weeks. The free version offers the advantage of unlimited queries, but it is relatively less accurate compared to the paid version. Additionally, there is the inconvenience of manually logging in and updating to the newly uploaded files.
GeoLite2-ASN.mmdb / 7.83 MB
GeoLite2-City.mmdb / 68.4 MB
GeoLite2-Country.mmdb / 5.91 MB

build.gradle.kts

  • Add the following content to the build.gradle.kts file in the project root:
dependencies {
    implementation("com.maxmind.geoip2:geoip2:4.1.0")
}

GeoLocationConfig.kt

  • First, the DatabaseReader class is registered as a Spring singleton bean.
import com.maxmind.db.CHMCache
import com.maxmind.geoip2.DatabaseReader
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.io.File

@Configuration
class GeoLocationConfig {

    @Bean("databaseReader")
    fun databaseReader(): DatabaseReader {

        return DatabaseReader
            .Builder(File("/GeoLite2-City.mmdb"))
            .withCache(CHMCache())
            .build()
    }
}
  • The location of the previously downloaded local database file is assumed to be at the root directory /. You can freely specify this according to your project's situation.
  • The reason for registering it as a singleton bean is to reuse the CHMCache instance, which acts as a local cache, during the application's runtime. Once an IP address is requested, it can be stored up to 2,000 entries, allowing responses from the local cache instead of querying the local database file again. When the cache exceeds 2,000 entries, the oldest ones are removed.

GeoLocationService.kt

  • The GeoLocationService class is created as follows. It has the role of querying and returning Geolocation information based on an IP address.
import com.maxmind.geoip2.DatabaseReader
import com.maxmind.geoip2.model.CityResponse
import com.maxmind.geoip2.record.Country
import org.springframework.stereotype.Service
import java.io.Serializable
import java.net.InetAddress

@Service
class GeoLocationService(
    private val databaseReader: DatabaseReader
) {
    fun getGeoLocation(ipAddress: String?): GeoLocationDTO? {

        if (ipAddress.isNullOrBlank()) return null

        return try {

            val response: CityResponse = databaseReader.city(InetAddress.getByName(ipAddress))
            val country: Country = response.country
            val subdivision = response.getMostSpecificSubdivision()

            GeoLocationDTO(
                ipAddress = ipAddress,
                country = country.name,
                countryCode = country.isoCode,
                subdivision = subdivision.name,
                subdivisionCode = subdivision.isoCode
            )

        } catch (ex: Exception) {
            null
        }
    }
}

data class GeoLocationDTO(

    var ipAddress: String? = null,
    var country: String? = null,
    var countryCode: String? = null,
    var subdivision: String? = null,
    var subdivisionCode: String? = null

) : Serializable

Usage Example

  • The previously created service bean can be used as follows:
// country: South Korea, country_code: KR, subdivision: Seoul, subdivision_code: 11
val geolocation = geoLocationService.getGeoLocation({ip-address})

Production Implementation Experience

  • The most critical factor in the implementation is the accuracy of the IP Geolocation information. The producer, MaxMind, also advises that using geolocation information based on IP addresses can inherently be inaccurate and should not be used seriously in business models. In actual production use, responding to customer inflow, the accuracy for the Country (e.g., Korea) was found to be 100%, and no frequent errors have been experienced for Subdivision (e.g., Seoul) so far. However, the City (e.g., Gangnam-gu) data was too inconsistent to be reliable, sometimes even incorrect for the location of my own office. Therefore, in the examples in this article, I only covered up to the retrieval of Country and Subdivision.
  • The next important factor is query speed and load. It was crucial for me as it was used in the preprocessing stage of every API request in the backend. Since it's a local database based on SQLite, there is no network load. It can always be included in the latest version of the backend's Docker image during the build. With local caching activated as in the example, the query speed in production use was extremely fast, with 99.9% of queries responding in 0ms. Only very occasionally did it respond in less than 10ms. Ultimately, it settled into production without any issues.

Reference Articles