Open-source project
maxmind/GeoIP2-java avatar
maxmind/GeoIP2-java

GeoIP2-java: MaxMind's geolocation database reader and web service client for Java

Java API for GeoIP2 webservice client and database reader

864 stars210 forksJavaApache-2.0

At a glance

What is it?
GeoIP2-java is the official Java API for MaxMind's GeoIP and GeoLite web services and for reading the MMDB databases locally. The web service client is thread-safe and reuses pooled HTTP connections, and it retries once on transport failures while deliberately not retrying timeouts, cancellations or configuration errors, so a bug in your account details surfaces immediately instead of after a backoff.
Who is it for?
Adopt GeoIP2-java when you need country, city or subdivision lookups in a Java service and want either the hosted API or local MMDB files through one interface. Do not treat a geolocation result as an address, because the README states the data is inherently imprecise and must not be used to identify a particular address or household.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the library provides and how it is installed

GeoIP2-java provides an API for the GeoIP and GeoLite web services and databases. The repository description calls it a Java API for the GeoIP2 web service client and database reader, and the licence is Apache-2.0 with the project page at maxmind.github.io/GeoIP2-java.

Installation has three documented routes. Maven is the recommended one, with the dependency added to your pom.xml:

xml
    <dependency>
        <groupId>com.maxmind.geoip2</groupId>
        <artifactId>geoip2</artifactId>
        <version>5.2.0</version>
    </dependency>

Gradle is the second, with the coordinates added to your build.gradle:

code
repositories {
    mavenCentral()
}
dependencies {
    implementation 'com.maxmind.geoip2:geoip2:5.2.0'
}

The third is the fallback for people who cannot use either build tool: include the geoip2.jar file and its dependencies in your classpath, downloading the JAR files from the GitHub releases page.

The two data sources are the GeoIP and GeoLite families, and the distinction runs through the whole API. The web service client takes an accountId and a licenseKey, and the host method on the builder switches between them, so the same code can point at the production GeoIP service, the GeoLite service by setting the host to geolite.info, or the Sandbox service by setting it to sandbox.maxmind.com. The Sandbox option is the one worth knowing about, because it lets you develop against realistic responses without consuming production quota.

The release history shows steady maintenance: v5.2.0 in July 2026, v5.1.0 in May and v5.0.2 in December 2025. The last push was on 2026-10-19.

The precision caveat that should shape your design

The README has a short section on IP geolocation usage, and it is the most important paragraph in the document for anyone building on this data.

IP geolocation is inherently imprecise. Locations are often near the center of the population. Any location provided by a GeoIP database or web service should not be used to identify a particular address or household.

That has direct architectural consequences. A geolocation lookup answers the question what country is this connection from, or roughly which city, and it does not answer where the person lives. The second point is the one with legal weight in many jurisdictions, since treating an IP-derived location as an address is how geolocation-based enforcement goes wrong.

The practical pattern that follows is to use the country code for policy decisions, such as which content catalogue to serve or which sanctions list to check, and to treat the city and subdivision as a hint for display purposes rather than as a decision input. The library exposes exactly these levels separately, with a Country object carrying an isoCode and a name, and the country lookup returning names in multiple locales, so the decision you make on the country code does not depend on a translated string.

The README's own example prints the country name in English and in another locale from the same lookup, which shows the shape of the data: one country, many names, and a stable code underneath.

The web service client, retries and what is deliberately not retried

The most interesting part of the README in engineering terms is the retry policy, because it is unusually explicit about what it will not do.

The WebServiceClient reuses pooled HTTP connections for performance. The problem it creates is that idle connections can be silently closed by load balancers or other intermediaries, and when the next request reuses such a half-closed connection the JDK reports the failure as a Connection reset, a Broken pipe or a similar transport error.

To smooth over those intermittent failures, the SDK retries once by default. Most transport-level IOExceptions are retried. What it does not retry is listed explicitly.

Timeouts are not retried, including connect-phase timeouts, because the SDK honours the timeouts you configure rather than extending them. Cancellation is not retried, whether an InterruptedIOException or any interrupt observed before the request runs. And typically deterministic failures are not retried: UnknownHostException, ConnectException, SSLHandshakeException and SSLPeerUnverifiedException, on the reasoning that retrying these would just delay surfacing a config bug.

That last group is the design decision worth internalising. A wrong account ID or a bad license key produces an authentication failure on the server and a DNS or TLS failure if the hostname is wrong, and retrying either one turns a five-second error into a thirty-second error. Surfacing it immediately is kinder to whoever has to fix it.

HTTP 4xx and 5xx responses are surfaced through the existing exception hierarchy and are never retried. The README also notes that request bodies are replayable, so retried requests are byte-identical to the original, which is what makes a retry safe for a POST.

The retry budget is settable on the builder:

java
WebServiceClient client = new WebServiceClient.Builder(42, "license_key")
    .maxRetries(2) // up to two retries (three total attempts)
    .build();

Setting maxRetries to zero disables retrying entirely, and negative values throw IllegalArgumentException.

The client itself is safe to share across threads, and the README's advice is to reuse it so that new connections are not created for each request.

Connection pooling and the keep-alive timeout workaround

The connection reset problem has a second remedy beyond retries, and it is the more interesting one operationally.

If you frequently see Connection reset errors, the README says you can reduce the JDK's keep-alive timeout through the system property jdk.httpclient.keepalive.timeout, in seconds, to evict pooled connections before any intermediary does so.

That inverts the usual mental model. Rather than making your client tolerate a connection that a load balancer has quietly closed, you make the client close it first. The JDK's HTTP client pools connections and keeps them alive for a default period, and if that period is longer than the idle timeout of something in the network path, your client will eventually hand you a socket that the other end has already forgotten.

Tuning the property downwards means the client discards connections more eagerly, so the pool refills with fresh ones rather than handing you a stale socket. The cost is more handshakes, which for a geolocation lookup made occasionally is nothing at all.

This is worth understanding because the symptom appears as an intermittent failure at a rate of perhaps one request in a few hundred, which is the worst shape of bug. It looks like network flakiness, it does not correlate with anything in your application, and the natural first reaction is to add a general retry to your own code, which is what the SDK has already done for you at the right layer.

The combination the README describes is therefore: the SDK retries once by default for the transport errors that are worth retrying, and you shorten the keep-alive timeout if the resets persist. Neither requires you to catch exceptions in your own code.

The response model: country, city, subdivision and postal as separate records

The web service returns a model class for the endpoint you called, and that model in turn contains multiple record classes, each representing part of the returned data.

The country service example shows the smallest case:

java
WebServiceClient client = new WebServiceClient.Builder(42, "license_key")
    .build();

InetAddress ipAddress = InetAddress.getByName("128.101.101.101");

// Do the lookup
CountryResponse response = client.country(ipAddress);

The city endpoint returns the same country record and adds more. You get the country, then the most specific subdivision, then the city, then the postal record, each from its own accessor on the response. The subdivision is fetched through mostSpecificSubdivision rather than a list, which reflects the data model: a geolocation can have several subdivisions and the most specific one is the one that answers the question.

Every record carries the same shape of data. There is a name, an isoCode where one exists, and a names map holding the same name in several locales, which is how the same country can be rendered as United States in one place and as its local name in another. The example prints the English name and then the same name in a second locale from the same object.

The endpoint you choose determines the cost and the detail. A country lookup is cheaper and coarser than a city lookup, and for a use case like routing a request to the right regional catalogue, the country endpoint is enough.

Failures are uniform. If the request fails, the client class throws an exception, and the exception hierarchy covers both the HTTP status responses and the transport failures the retry policy declines to retry.

Local database reading, and the shape of the repository

The library covers both access paths. The web service is the network one, and the other is reading the MMDB databases locally, which is what the database reader half of the description refers to.

The local path exists for a reason that is easy to guess and worth stating: it removes the per-lookup network call and the per-lookup latency. A service doing millions of lookups pays a network round trip for each one unless it reads the database itself. The trade is that the database file has to be kept up to date, which is a deployment concern rather than a code one, and it is the reason the SDK treats the two paths as peers rather than as a fallback hierarchy.

The repository layout shows how the project is organised. There is a src/ directory for the implementation, a sample/ directory holding a Benchmark.java, docs/ for the generated documentation, dev-bin/ for developer scripts, and a pom.xml with checkstyle.xml and checkstyle-suppressions.xml for the lint rules. There is a lychee.toml for link checking, a mise.toml and mise.lock for toolchain pinning, a CHANGELOG.md, a CLAUDE.md, a README.dev.md for developer documentation separate from the user-facing README, and a .gitmodules file.

The separation between README.md and README.dev.md is a small sign of a maintained project, and so is the link checking configuration. The checkstyle setup is the other sign that this is a library with a published API surface rather than a script collection, which matches what MaxMind's other client libraries do.

The one thing the repository does not contain is the data itself. The databases are MaxMind's, and the web services are MaxMind's, so the library is the interface and the subscription is the product.

Limits: geolocation is approximate, the data is a subscription, and the API is MaxMind's

The limits fall into three groups, and the first is the one the README states most plainly.

Geolocation is approximate. Locations are often near the centre of the population, and the data must not be used to identify a particular address or household. Any application built on this library inherits that limitation, and the more precise the endpoint you use the more your users will expect precision that is not there.

The second group is commercial. GeoIP is a paid product with a per-lookup or subscription model, and GeoLite is the free tier with its own registration and its own limitations on volume and accuracy. The library abstracts the difference through the host method, but it does not abstract the cost. A high-traffic application doing a lookup per request needs the commercial tier or the local database, and either way there is a licence obligation attached to the data.

The third group is structural. This is MaxMind's own client, so the model classes, the endpoint names and the exception hierarchy are MaxMind's API surface. A different provider's data would need a different library, and a schema that assumes the MaxMind shape, with country and city and subdivision as separate records and an isoCode on each, is a MaxMind-shaped schema.

The retry policy is a limit in a quieter sense. Because the SDK does not retry timeouts, and honours the timeouts you configure, a lookup against a slow endpoint will fail at your deadline rather than being given more time. That is the correct behaviour, but it means your timeout configuration is effectively the retry budget for the requests that matter most.

And the local database path has its own operational cost. The database file is a download you have to refresh, and a service that silently reads a stale file will keep answering with old data while looking perfectly healthy.

GeoIP2-java against a plain HTTP client and against the other MaxMind client libraries

Two comparisons make the choice clearer.

The first is against calling the web service yourself with the JDK's HTTP client or with OkHttp. The API is a small JSON document over HTTPS, so a hand-written client is not much work: post the IP as JSON, parse the response, map a few fields. The difference in approach is the response model and the failure behaviour. This library gives you typed records with the locales already resolved and a country isoCode that does not depend on a translated string, and it gives you the exception hierarchy and the retry policy, including the decisions about what not to retry that are the same decisions you would otherwise have to make and get wrong.

The costs of using the library are the dependency and the fact that your objects are MaxMind's types. If you only need one field, the hand-written client is smaller.

The second comparison is against the other MaxMind client libraries, the ones for other languages. The difference in approach is minimal by design. MaxMind keeps the model shape and the semantics consistent across languages so that a team working across a polyglot stack sees the same concepts, and the retry policy described here is the kind of thing that is specified once and then implemented consistently. The Java library is one of a family rather than an independent implementation with its own ideas.

So the choice comes down to whether the data itself is worth the abstraction. If you are doing country-level lookups for routing or policy and the volume is low, a plain HTTP call is defensible. If you are doing city-level lookups at volume, with locale-aware names, a local database and a client that behaves predictably under network conditions, this library is the version of that decision someone has already made and maintained.

Editorial conclusion

Adopt GeoIP2-java when you need country, city or subdivision lookups in a Java service and want either the hosted API or local MMDB files through one interface. Do not treat a geolocation result as an address, because the README states the data is inherently imprecise and must not be used to identify a particular address or household. Verify first whether you want the paid GeoIP databases and web services or the free GeoLite ones, that your locale fallback order matches the languages you need to render, and that you reuse a single client instance rather than constructing one per request.

Frequently asked questions

How do I add the GeoIP2 Java library to my build?

With Maven, add the dependency with groupId com.maxmind.geoip2, artifactId geoip2 and the version you want. With Gradle, add the implementation dependency com.maxmind.geoip2:geoip2:5.2.0 to your build.gradle. If you cannot use either, the README says to include the geoip2.jar file and its dependencies in your classpath, downloaded from the GitHub releases page.

How does the WebServiceClient decide what to retry?

The SDK retries once by default for most transport-level IOExceptions, because pooled connections can be silently closed by intermediaries. It does not retry timeouts, since it honours the timeouts you configure, nor cancellation, nor deterministic failures such as UnknownHostException, ConnectException, SSLHandshakeException and SSLPeerUnverifiedException, because retrying those would delay surfacing a config bug. HTTP 4xx and 5xx responses are never retried.

Can I use the GeoIP2 web service for free?

Yes, through the GeoLite web services. The README says to set the host method on the WebServiceClient builder to geolite.info to use GeoLite instead of GeoIP, and to set it to sandbox.maxmind.com to use the Sandbox services for development. The builder also takes a timeout and a locales fallback order.

Official sources

  1. License: Apache-2.0
  2. maxmind/GeoIP2-java on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/maxmind-geoip2-java.svg)](https://hysenlabs.com/projects/maxmind-geoip2-java)

Was this useful?