# 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]](https://www.maxmind.com/en/accounts/current/geoip/downloads)
  * 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]](https://github.com/P3TERX/GeoLite.mmdb)

```bash
$ 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.

```kotlin
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:

```kotlin
dependencies {
    implementation("com.maxmind.geoip2:geoip2:4.1.0")
}
```

### GeoLocationConfig.kt
  * First, the `DatabaseReader` class is registered as a Spring singleton bean.

```kotlin
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.

```kotlin
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:

```kotlin
// 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
  * [MaxMind - GeoLite2 Free Geolocation Data](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data)
