diff --git a/router/java/src/com/maxmind/geoip2/DatabaseReader.java b/router/java/src/com/maxmind/geoip2/DatabaseReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3b97cca500058e2f53a9dc3a038231a134bccdf
--- /dev/null
+++ b/router/java/src/com/maxmind/geoip2/DatabaseReader.java
@@ -0,0 +1,329 @@
+package com.maxmind.geoip2;
+
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.maxmind.db.*;
+import com.maxmind.db.Reader.FileMode;
+import com.maxmind.geoip2.exception.AddressNotFoundException;
+import com.maxmind.geoip2.exception.GeoIp2Exception;
+import com.maxmind.geoip2.model.*;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>
+ * The class {@code DatabaseReader} provides a reader for the GeoIP2 database
+ * format.
+ * </p>
+ * <h3>Usage</h3>
+ * <p>
+ * To use the database API, you must create a new {@code DatabaseReader} using
+ * the {@code DatabaseReader.Builder}. You must provide the {@code Builder}
+ * constructor either an {@code InputStream} or {@code File} for your GeoIP2
+ * database. You may also specify the {@code fileMode} and the {@code locales}
+ * fallback order using the methods on the {@code Builder} object. After you
+ * have created the {@code DatabaseReader}, you may then call the appropriate
+ * method (e.g., {@code city}) for your database, passing it the IP address
+ * you want to look up.
+ * </p>
+ * <p>
+ * If the lookup succeeds, the method call will return a response class for
+ * the GeoIP2 lookup. The class in turn contains multiple record classes,
+ * each of which represents part of the data returned by the database.
+ * </p>
+ * <p>
+ * We recommend reusing the {@code DatabaseReader} object rather than creating
+ * a new one for each lookup. The creation of this object is relatively
+ * expensive as it must read in metadata for the file. It is safe to share the
+ * object across threads.
+ * </p>
+ * <h4>Caching</h4>
+ * <p>
+ * The database API supports pluggable caching (by default, no caching is
+ * performed). A simple implementation is provided by
+ * {@code com.maxmind.db.CHMCache}.  Using this cache, lookup performance is
+ * significantly improved at the cost of a small (~2MB) memory overhead.
+ * </p>
+ */
+public class DatabaseReader implements DatabaseProvider, Closeable {
+
+    private final Reader reader;
+
+    private final ObjectMapper om;
+
+    private final List<String> locales;
+
+    private DatabaseReader(Builder builder) throws IOException {
+        if (builder.stream != null) {
+            this.reader = new Reader(builder.stream, builder.cache);
+        } else if (builder.database != null) {
+            this.reader = new Reader(builder.database, builder.mode, builder.cache);
+        } else {
+            // This should never happen. If it does, review the Builder class
+            // constructors for errors.
+            throw new IllegalArgumentException(
+                    "Unsupported Builder configuration: expected either File or URL");
+        }
+        this.om = new ObjectMapper();
+        this.om.configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, false);
+        this.om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
+                false);
+        this.om.configure(
+                DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
+        this.locales = builder.locales;
+    }
+
+    /**
+     * <p>
+     * Constructs a Builder for the {@code DatabaseReader}. The file passed to
+     * it must be a valid GeoIP2 database file.
+     * </p>
+     * <p>
+     * {@code Builder} creates instances of {@code DatabaseReader}
+     * from values set by the methods.
+     * </p>
+     * <p>
+     * Only the values set in the {@code Builder} constructor are required.
+     * </p>
+     */
+    public static final class Builder {
+        final File database;
+        final InputStream stream;
+
+        List<String> locales = Collections.singletonList("en");
+        FileMode mode = FileMode.MEMORY_MAPPED;
+        NodeCache cache = NoCache.getInstance();
+
+        /**
+         * @param stream the stream containing the GeoIP2 database to use.
+         */
+        public Builder(InputStream stream) {
+            this.stream = stream;
+            this.database = null;
+        }
+
+        /**
+         * @param database the GeoIP2 database file to use.
+         */
+        public Builder(File database) {
+            this.database = database;
+            this.stream = null;
+        }
+
+        /**
+         * @param val List of locale codes to use in name property from most
+         *            preferred to least preferred.
+         * @return Builder object
+         */
+        public Builder locales(List<String> val) {
+            this.locales = val;
+            return this;
+        }
+
+        /**
+         * @param cache backing cache instance
+         * @return Builder object
+         */
+        public Builder withCache(NodeCache cache) {
+            this.cache = cache;
+            return this;
+        }
+
+        /**
+         * @param val The file mode used to open the GeoIP2 database
+         * @return Builder object
+         * @throws java.lang.IllegalArgumentException if you initialized the Builder with a URL, which uses
+         *                                            {@link FileMode#MEMORY}, but you provided a different
+         *                                            FileMode to this method.
+         */
+        public Builder fileMode(FileMode val) {
+            if (this.stream != null && FileMode.MEMORY != val) {
+                throw new IllegalArgumentException(
+                        "Only FileMode.MEMORY is supported when using an InputStream.");
+            }
+            this.mode = val;
+            return this;
+        }
+
+        /**
+         * @return an instance of {@code DatabaseReader} created from the
+         * fields set on this builder.
+         * @throws IOException if there is an error reading the database
+         */
+        public DatabaseReader build() throws IOException {
+            return new DatabaseReader(this);
+        }
+    }
+
+    /**
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return A <T> object with the data for the IP address
+     * @throws IOException              if there is an error opening or reading from the file.
+     * @throws AddressNotFoundException if the IP address is not in our database
+     */
+    private <T> T get(InetAddress ipAddress, Class<T> cls,
+                      String type) throws IOException, AddressNotFoundException {
+
+        String databaseType = this.getMetadata().getDatabaseType();
+        if (!databaseType.contains(type)) {
+            String caller = Thread.currentThread().getStackTrace()[2]
+                    .getMethodName();
+            throw new UnsupportedOperationException(
+                    "Invalid attempt to open a " + databaseType
+                            + " database using the " + caller + " method");
+        }
+
+        ObjectNode node = jsonNodeToObjectNode(reader.get(ipAddress));
+
+        // We throw the same exception as the web service when an IP is not in
+        // the database
+        if (node == null) {
+            throw new AddressNotFoundException("The address "
+                    + ipAddress.getHostAddress() + " is not in the database.");
+        }
+
+        InjectableValues inject = new JsonInjector(locales, ipAddress.getHostAddress());
+
+        return this.om.reader(inject).treeToValue(node, cls);
+    }
+
+    private ObjectNode jsonNodeToObjectNode(JsonNode node)
+            throws InvalidDatabaseException {
+        if (node == null || node instanceof ObjectNode) {
+            return (ObjectNode) node;
+        }
+        throw new InvalidDatabaseException(
+                "Unexpected data type returned. The GeoIP2 database may be corrupt.");
+    }
+
+    /**
+     * <p>
+     * Closes the database.
+     * </p>
+     * <p>
+     * If you are using {@code FileMode.MEMORY_MAPPED}, this will
+     * <em>not</em> unmap the underlying file due to a limitation in Java's
+     * {@code MappedByteBuffer}. It will however set the reference to
+     * the buffer to {@code null}, allowing the garbage collector to
+     * collect it.
+     * </p>
+     *
+     * @throws IOException if an I/O error occurs.
+     */
+    @Override
+    public void close() throws IOException {
+        this.reader.close();
+    }
+
+    @Override
+    public CountryResponse country(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, CountryResponse.class, "Country");
+    }
+
+    @Override
+    public CityResponse city(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, CityResponse.class, "City");
+    }
+
+    /**
+     * Look up an IP address in a GeoIP2 Anonymous IP.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return a AnonymousIpResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, AnonymousIpResponse.class, "GeoIP2-Anonymous-IP");
+    }
+
+    /**
+     * Look up an IP address in a GeoLite2 ASN database.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return an AsnResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public AsnResponse asn(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, AsnResponse.class, "GeoLite2-ASN");
+    }
+
+    /**
+     * Look up an IP address in a GeoIP2 Connection Type database.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return a ConnectTypeResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public ConnectionTypeResponse connectionType(InetAddress ipAddress)
+            throws IOException, GeoIp2Exception {
+        return this.get(ipAddress, ConnectionTypeResponse.class,
+                "GeoIP2-Connection-Type");
+    }
+
+    /**
+     * Look up an IP address in a GeoIP2 Domain database.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return a DomainResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public DomainResponse domain(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this
+                .get(ipAddress, DomainResponse.class, "GeoIP2-Domain");
+    }
+
+    /**
+     * Look up an IP address in a GeoIP2 Enterprise database.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return an EnterpriseResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public EnterpriseResponse enterprise(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, EnterpriseResponse.class, "Enterprise");
+    }
+
+
+    /**
+     * Look up an IP address in a GeoIP2 ISP database.
+     *
+     * @param ipAddress IPv4 or IPv6 address to lookup.
+     * @return an IspResponse for the requested IP address.
+     * @throws GeoIp2Exception if there is an error looking up the IP
+     * @throws IOException     if there is an IO error
+     */
+    @Override
+    public IspResponse isp(InetAddress ipAddress) throws IOException,
+            GeoIp2Exception {
+        return this.get(ipAddress, IspResponse.class, "GeoIP2-ISP");
+    }
+
+    /**
+     * @return the metadata for the open MaxMind DB file.
+     */
+    public Metadata getMetadata() {
+        return this.reader.getMetadata();
+    }
+}