Have you ever get the above pop up and how fast do you click block? Asking for a user location in a browser is huge friction (at least for me) because in this age, privacy is expensive and I’m not gonna add a list of website that already knows my info just to know where the closest hot single mom lululemon store is.
So the solution is to get user location via an information that they already provide voluntarily without any additional friction, which is an IP address. There are lots of services that revolves around that specific problem and most of them are paid. I’m unemployed and got no big capital to spend on this, so after searching, I found MaxMind, they provide a GeoIP databases that I could deploy on my own server for free. Granted that it’s not gonna be as accurate as the paid version but for my use case, a coarse detection is good enough. I don’t need to know that the user is currently sitting on a toilet seat inside their home, a city level accuracy is more than enough.
The result I care about is simple: latitude, longitude, and a “this is my city-ish” expectation. I don’t pretend it’s GPS. Broadband users usually land in the right city. Mobile users sometimes drift because of carrier routing. VPN users occasionally show up in a data center. That’s fine because I only use it to center the viewport. The map opens at a sensible zoom over the likely city, and the very first affordance the user sees is an invitation to drag or search. If my guess is off by a neighborhood (or a country), they can correct it themselves.
Reverse proxies and CDNs add their own chaos, so I only trust a canonical client-IP header that I control at the edge. Cloudflare’s CF-Connecting-IP, for example and ignore random X-Forwarded-For nonsense. IPv6 is first class, not an afterthought, plenty of users show up v6 only now. If the incoming address is private or reserved, I skip the lookup and open a neutral default instead of ramming the camera to (0, 0) and pray that it doesn’t make them go away.
Now let’s get a bit technical. The “database” I’m using is not a database server at all. It’s a single binary file GeoLite2-City.mmdb that sits next to my app. At startup I open it with MaxMind’s reader library and keep that handle around. Every request that comes in with an IP gets a local lookup. No network calls, no SQL, no migrations, no schema, no ORM. It’s just a fast key value read from a file format designed specifically for IP prefix lookups. MaxMind literally built the format to be quick under load and recommends the binary .mmdb for production-grade, high-volume lookups.
Under the hood, what lives inside that file is basically an efficient tree indexed by IPv4 and IPv6 subnets. You feed it an address, it walks the tree and hands back a compact record with city/region/country. Since it’s read-only, you don’t “write” anything to it, you update by swapping in a newer file once a month when MaxMind refreshes their datasets. I treat it like static assets with benefits: versioned, cacheable, and opened once per process.
Here’s a minimal working example that you could try out
// Setup the database reader object to be used in other files
import com.maxmind.geoip2.DatabaseReader;
@Configuration
public class GeoLocationConfig {
// Define an env var somewhere in your app to the location of .mmdb file
@Value("${geoip.db.path:GeoLite2-City.mmdb}")
private String geoIpDbPath;
// Using bean annotation for injection later using @Autowired
@Bean
public DatabaseReader getDatabaseReader() throws IOException {
File database = new File(geoIpDbPath);
if (!database.exists()) {
throw new IOException("GeoIP database file not found at: " + database.getAbsolutePath());
}
return new DatabaseReader.Builder(database).build();
}
}
Put the above code under your config folder, now we have to create a service that actually use the bean we have defined in the config
import com.maxmind.geoip2.DatabaseReader;
@Service
public class GeoLocationService {
// Since we have defined a bean of type DatabaseReader, this will be injected here
@Autowired
private DatabaseReader databaseReader;
public double[] getLatLngFromIp(String ip) throws IOException, GeoIp2Exception, UnknownHostException {
try {
CityResponse cityResponse = databaseReader.city(InetAddress.getByName(ip));
log.warn("City result {}", cityResponse);
double lat = cityResponse.getLocation().getLatitude();
double lng = cityResponse.getLocation().getLongitude();
return new double[] {lat, lng};
} catch(GeoIp2Exception e) {
// Change 0, 0 to your default lat/lng
return new double[] {0, 0};
}
}
}
The above example is using a bean to make an instance and will be used throughout the life of the app. Since it’s a singleton, you don’t have to fiddle around with opening and closing the handler.
This solution is perfect for me. I avoid the “Allow location?” conversion blackhole, spend zero per lookup, keep my request path entirely local to avoid unnecessary latency, and give the user a reasonable starting view. They can drag the viewport or search for precision, and I never had to migrate a table or babysit a microservice to make that happen. That’s the charm of .mmdb, it’s a file, not a project.

Leave a reply to Securing Forms Against Brute Force Abuse – From Code to Kanji Cancel reply