first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ac1ad8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,65 @@
+# Eclipse project files 
+.classpath
+.project
+.settings/
+.checkstyle
+
+# IntelliJ project files
+*.iml
+*.ipr
+*.iws
+
+# Additional tools
+.clover/
+
+# OSX files
+.DS_Store
+
+# Compiler output, class files
+*.class
+bin/
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+#Test config and output
+/target/
+test-output/
+local/
+reports/
+.pmd
+tmp/
+
+# Build artifacts
+out/
+build/
+jarsIn/
+*.jar
+build.xml
+.idea
+*.properties
+*.releaseBackup
+*.next
+*.tag
+
+# Jekyll
+_site/
+_*
+_*/
diff --git a/DISCLAIMER-WIP b/DISCLAIMER-WIP
new file mode 100644
index 0000000..ae9f942
--- /dev/null
+++ b/DISCLAIMER-WIP
@@ -0,0 +1,26 @@
+Apache DataSketches (incubating) is an effort undergoing incubation 
+at The Apache Software Foundation (ASF), sponsored by the Apache Incubator. 
+
+Incubation is required of all newly accepted projects until a further review 
+indicates that the infrastructure, communications, and decision making process 
+have stabilized in a manner consistent with other successful ASF projects.
+
+While incubation status is not necessarily a reflection of the
+completeness or stability of the code, it does indicate that the
+project has yet to be fully endorsed by the ASF.
+
+Some of the incubating project's releases may not be fully compliant
+with ASF policy. For example, releases may have incomplete or
+un-reviewed licensing conditions. What follows is a list of known
+issues the project is currently aware of (note that this list, by
+definition, is likely to be incomplete): 
+
+ * No issues are known at this time. 
+
+If you are planning to incorporate this work into your
+product or project, please be aware that you will need to conduct a
+thorough licensing review to determine the overall implications of
+including this work. For the current status of this project through the Apache
+Incubator visit: 
+
+http://incubator.apache.org/projects/datasketches.html
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f927b76
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,210 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+
+
+APPENDIX A: How to apply the Apache License to your work.
+
+    To apply the Apache License to your work, attach the following
+    boilerplate notice, with the fields enclosed by brackets "[]"
+    replaced with your own identifying information. (Don't include
+    the brackets!)  The text should be enclosed in the appropriate
+    comment syntax for the file format. We also recommend that a
+    file or class name and description of purpose be included on the
+    same "printed page" as the copyright notice for easier
+    identification within third-party archives.
+    -------------------------------------------------------------
+    Copyright [yyyy] [name of copyright owner]
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+    -------------------------------------------------------------
+
+
+
+APPENDIX B: Additional licenses relevant to this product:
+    (none)
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c8912d6
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache DataSketches Server (incubating)
+Copyright 2020 - The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f44901
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# DataSketches Server
+This is a very simple, container-friendly web server with a JSON API. The provided
+server can be used to experiment with sketches or to add sketch functionality to a
+project without natively integrating with the DataSketches library.
+
+This is not intended to provide a high-performance database; there are already
+existing integrations with PostgreSQL or Druid for such cases. The focus here 
+is on ease-of-use rather than speed.
+
+The repository is still in a very early state and lacks both unit tests and
+robust documentation.
+
+----
+
+Disclaimer: Apache DataSketches is an effort undergoing incubation at The Apache
+Software Foundation (ASF), sponsored by the Apache Incubator. Incubation is required
+of all newly accepted projects until a further review indicates that the infrastructure,
+communications, and decision making process have stabilized in a manner consistent
+with other successful ASF projects. While incubation status is not necessarily a
+reflection of the completeness or stability of the code, it does indicate that the
+project has yet to be fully endorsed by the ASF.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4a9d6d4
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.datasketches</groupId>
+    <artifactId>datasketches-server</artifactId>
+    <packaging>jar</packaging>
+    <version>0.1.0</version>
+    <name>datasketches-server</name>
+    <url>https://datasketches.apache.org</url>
+    <inceptionYear>2020</inceptionYear>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.6</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- DataSketches dependencies -->
+        <dependency>
+            <groupId>org.apache.datasketches</groupId>
+            <artifactId>datasketches-memory</artifactId>
+            <version>1.2.0-incubating</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.datasketches</groupId>
+            <artifactId>datasketches-java</artifactId>
+            <version>1.3.0-incubating</version>
+        </dependency>
+
+        <!--Jetty dependencies-->
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>9.4.31.v20200723</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>9.4.31.v20200723</version>
+        </dependency>
+
+        <!--Testing-->
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.14.3</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}-${project.version}</finalName>
+        <directory>target</directory>
+
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.datasketches.server.SketchServer</mainClass>
+                        </manifest>
+                    </archive>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+	    <!--
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>findbugs-maven-plugin</artifactId>
+                <version>2.0</version>
+                <configuration>
+                    <findbugsXmlOutput>true</findbugsXmlOutput>
+                    <xmlOutput>true</xmlOutput>
+                    <threshold>High</threshold>
+                </configuration>
+            </plugin>
+	    -->
+        </plugins>
+    </build>
+    
+    <reporting>
+        <plugins>
+	    <!--
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>findbugs-maven-plugin</artifactId>
+                <version>3.0.0</version>
+            </plugin>
+	    -->
+        </plugins>
+    </reporting>
+
+    <repositories>
+          <repository>
+      <id>apache.snapshots</id>
+      <name>Apache Snapshot Repository</name>
+      <url>https://repository.apache.org/content/groups/snapshots/org/apache/datasketches/</url>
+      <releases>
+        <enabled>false</enabled>
+      </releases>
+      <snapshots>
+        <enabled>true</enabled>
+      </snapshots>
+    </repository>
+    <repository>
+      <id>apache</id>
+      <name>Apache Releases Repository</name>
+      <url>https://repository.apache.org/content/repositories/releases/org/apache/datasketches/</url>
+      <releases>
+        <enabled>true</enabled>
+      </releases>
+      <snapshots>
+        <enabled>false</enabled>
+      </snapshots>
+    </repository>
+    </repositories>
+</project>
diff --git a/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
new file mode 100644
index 0000000..bad4af0
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import org.apache.datasketches.Family;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ * Provides a common request handler for the sketches server. This gives us several benefits:
+ * <ul>
+ *   <li>Extracts JSON query from querystring or POST body, as appropriate, to allow multiple input types.</li>
+ *   <li>Sketches are stateful, and even reading can be disrupted by writes on another thread. A real
+ *       database would have a more sophisticated locking system, but this class lets us syncrhonize across
+ *       query types.</li>
+ *   <li>Handles both JSON arrays or single JSON objects as inputs, letting the query handlers avoid
+ *       code duplication.
+ * </ul>
+ * By using this class, the individual query handlers are able to consume and emit only JSON objects; they
+ * need not worry about details of the HTTP request or response.
+ */
+public abstract class BaseSketchesQueryHandler extends AbstractHandler {
+  final SketchStorage sketches;
+  final boolean queryExempt;
+
+  /**
+   * Basic query handler. Assumes calls must include a JSON query.
+   * @param sketches The sketches database to use
+   */
+  BaseSketchesQueryHandler(SketchStorage sketches) {
+    this(sketches, false);
+  }
+
+  /**
+   * Basic query handler, allowing the derived type to explicitly declare if an input query is optional.
+   * @param sketches The sketches database to use
+   * @param queryExempt <tt>true</tt> if a query is not required, otherwise <tt>false</tt>
+   */
+  BaseSketchesQueryHandler(SketchStorage sketches, boolean queryExempt) {
+    if (sketches == null) {
+      throw new IllegalArgumentException("Cannot initialize handler with SketchStorage == null");
+    }
+    this.sketches = sketches;
+    this.queryExempt = queryExempt;
+  }
+
+  JsonElement checkMethodAndReadJson(Request baseRequest,
+                                     HttpServletRequest request,
+                                     HttpServletResponse response) throws IOException {
+    JsonElement query = null;
+    if (request.getMethod().equals("POST")) {
+      response.setContentType("application/json");
+      query = JsonParser.parseReader(request.getReader());
+    } else if (request.getMethod().equals("GET")) {
+      response.setContentType("text/html");
+      query = JsonParser.parseString(URLDecoder.decode(request.getQueryString(), "utf-8"));
+    } else {
+      response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      baseRequest.setHandled(true);
+    }
+
+    return query;
+  }
+
+  /**
+   * Query handler to be implemented by subclasses
+   * @param query A JSON query to process
+   * @return A JSON response
+   */
+  protected abstract JsonObject processQuery(JsonObject query);
+
+  /**
+   * Internal method to synchronize calls to subclasses
+   * @param query A JSON query to process
+   * @return A JSON response
+   */
+  final synchronized JsonObject callProcessQuery(JsonObject query) {
+    return processQuery(query);
+  }
+
+  @Override
+  public void handle(String target,
+                     Request baseRequest,
+                     HttpServletRequest request,
+                     HttpServletResponse response) throws IOException {
+    JsonElement query = null;
+    if (!queryExempt && ((query = checkMethodAndReadJson(baseRequest, request, response)) == null)) {
+      return;
+    }
+
+    // error messages will be wrapped in json
+    response.setCharacterEncoding("utf-8");
+    response.setContentType("application/json");
+
+    JsonElement result = null;
+
+    try {
+      if (query == null) {
+        result = callProcessQuery(null);
+      } else if (query.isJsonArray()) {
+        for (JsonElement subQuery : query.getAsJsonArray()) {
+          JsonObject subResult = callProcessQuery(subQuery.getAsJsonObject());
+          if (subResult != null) {
+            // lazy initialization to avoid possibly empty array
+            if (result == null) {
+              result = new JsonArray(((JsonArray) query).size());
+            }
+            ((JsonArray) result).add(subResult);
+          }
+        }
+      } else {
+        result = callProcessQuery((JsonObject) query);
+      }
+
+      if (result != null) {
+        response.getWriter().print(result.toString());
+        //response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+      }
+
+      // we're ok if we reach here without an exception
+      response.setStatus(HttpServletResponse.SC_OK);
+    } catch (Exception e) {
+      JsonObject error = new JsonObject();
+      error.addProperty(ERROR_KEY, e.getMessage());
+      response.setStatus(UNPROCESSABLE_ENTITY);
+    }
+
+    baseRequest.setHandled(true);
+  }
+
+  static Family familyFromString(String type) throws IllegalArgumentException {
+    switch (type.toLowerCase()) {
+      case SKETCH_FAMILY_THETA:
+        return Family.QUICKSELECT;
+
+      case SKETCH_FAMILY_KLL:
+        return Family.KLL;
+
+      case SKETCH_FAMILY_FREQUENCY:
+        return Family.FREQUENCY;
+
+      case SKETCH_FAMILY_HLL:
+        return Family.HLL;
+
+      case SKETCH_FAMILY_CPC:
+        return Family.CPC;
+
+      case SKETCH_FAMILY_RESERVOIR:
+        return Family.RESERVOIR;
+
+      case SKETCH_FAMILY_VAROPT:
+        return Family.VAROPT;
+
+      default:
+        throw new IllegalArgumentException("Unrecognized sketch type: " + type);
+    }
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/DataQueryHandler.java b/src/main/java/org/apache/datasketches/server/DataQueryHandler.java
new file mode 100644
index 0000000..68255e4
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/DataQueryHandler.java
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import org.apache.datasketches.Family;
+import org.apache.datasketches.SketchesException;
+import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.frequencies.ErrorType;
+import org.apache.datasketches.frequencies.ItemsSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSamples;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.CompactSketch;
+import org.apache.datasketches.theta.Union;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ * Allows querying the sketches to obtain estimates of the relevant quantities.
+ *
+ * Full description to be added later.
+ */
+public class DataQueryHandler extends BaseSketchesQueryHandler {
+  public DataQueryHandler(SketchStorage sketches) {
+    super(sketches, false);
+  }
+
+  @Override
+  protected JsonObject processQuery(JsonObject query) {
+    if (!query.has(QUERY_NAME_FIELD)) {
+      throw new IllegalArgumentException("Query missing sketch name field");
+    }
+
+    String key = query.get(QUERY_NAME_FIELD).getAsString();
+    if (!sketches.contains(key)) {
+      throw new IllegalArgumentException("Invalid sketch name: " + key);
+    }
+
+    SketchStorage.SketchEntry se = sketches.getSketch(key);
+    JsonObject result;
+
+    switch (se.family) {
+      case UNION:
+      case HLL:
+      case CPC:
+        result = processDistinctQuery(query, se.family, se.sketch);
+        break;
+
+      case KLL:
+        result = processQuantilesQuery(query, se.family, se.sketch);
+        break;
+
+      case FREQUENCY:
+        result = processFrequencyQuery(query, se.family, se.sketch);
+        break;
+
+      case RESERVOIR:
+      case VAROPT:
+        result = processSamplingQuery(query, se.family, se.sketch);
+        break;
+
+      default:
+        throw new IllegalStateException("Unexpected sketch family: " + se.family);
+    }
+
+    return result;
+  }
+
+  private boolean checkSummaryFlag(JsonObject query) {
+    boolean addSummary = false;
+    if (query != null && query.has(QUERY_SUMMARY_FIELD)) {
+      addSummary = query.get(QUERY_SUMMARY_FIELD).getAsBoolean();
+    }
+    return addSummary;
+  }
+
+  private JsonObject processDistinctQuery(JsonObject query, Family type, Object sketch) {
+    if (query == null || type == null || sketch == null) {
+      return null;
+    }
+
+    // check if we need a summary
+    boolean addSummary = checkSummaryFlag(query);
+    String summary = null;
+
+    double estimate;
+    boolean isEstimationMode;
+    double p1StdDev;
+    double p2StdDev;
+    double p3StdDev;
+    double m1StdDev;
+    double m2StdDev;
+    double m3StdDev;
+
+    switch (type) {
+      case UNION:
+        CompactSketch thetaSketch = ((Union) sketch).getResult();
+        isEstimationMode = thetaSketch.isEstimationMode();
+        estimate = thetaSketch.getEstimate();
+        p1StdDev = thetaSketch.getUpperBound(1);
+        p2StdDev = thetaSketch.getUpperBound(2);
+        p3StdDev = thetaSketch.getUpperBound(3);
+        m1StdDev = thetaSketch.getLowerBound(1);
+        m2StdDev = thetaSketch.getLowerBound(2);
+        m3StdDev = thetaSketch.getLowerBound(3);
+        if (addSummary) { summary = thetaSketch.toString(); }
+        break;
+
+      case CPC:
+        CpcSketch cpcSketch = (CpcSketch) sketch;
+        isEstimationMode = true; // no exact mode
+        estimate = cpcSketch.getEstimate();
+        p1StdDev = cpcSketch.getUpperBound(1);
+        p2StdDev = cpcSketch.getUpperBound(2);
+        p3StdDev = cpcSketch.getUpperBound(3);
+        m1StdDev = cpcSketch.getLowerBound(1);
+        m2StdDev = cpcSketch.getLowerBound(2);
+        m3StdDev = cpcSketch.getLowerBound(3);
+        if (addSummary) { summary = cpcSketch.toString(); }
+        break;
+
+      case HLL:
+        HllSketch hllSketch = (HllSketch) sketch;
+        isEstimationMode = hllSketch.isEstimationMode();
+        estimate = hllSketch.getEstimate();
+        p1StdDev = hllSketch.getUpperBound(1);
+        p2StdDev = hllSketch.getUpperBound(2);
+        p3StdDev = hllSketch.getUpperBound(3);
+        m1StdDev = hllSketch.getLowerBound(1);
+        m2StdDev = hllSketch.getLowerBound(2);
+        m3StdDev = hllSketch.getLowerBound(3);
+        if (addSummary) { summary = hllSketch.toString(); }
+        break;
+
+      default:
+        throw new IllegalArgumentException("Unknown distinct counting sketch type: " + type.toString());
+    }
+
+    JsonObject result = new JsonObject();
+    result.addProperty(RESPONSE_ESTIMATE_FIELD, estimate);
+    result.addProperty(RESPONSE_ESTIMATION_MODE_FIELD, isEstimationMode);
+    result.addProperty(RESPONSE_P1STDEV_FIELD, p1StdDev);
+    result.addProperty(RESPONSE_P2STDEV_FIELD, p2StdDev);
+    result.addProperty(RESPONSE_P3STDEV_FIELD, p3StdDev);
+    result.addProperty(RESPONSE_M1STDEV_FIELD, m1StdDev);
+    result.addProperty(RESPONSE_M2STDEV_FIELD, m2StdDev);
+    result.addProperty(RESPONSE_M3STDEV_FIELD, m3StdDev);
+    if (addSummary)
+      result.addProperty(RESPONSE_SUMMARY_FIELD, summary);
+
+    return result;
+  }
+
+  private JsonObject processQuantilesQuery(JsonObject query, Family type, Object sketch) {
+    if (query == null || type == null || sketch == null) {
+      return null;
+    }
+
+    // check if we need a summary
+    boolean addSummary = checkSummaryFlag(query);
+    String summary = null;
+
+    boolean isEstimationMode;
+    float maxValue;
+    float minValue;
+    long streamLength;
+
+    double[] fractions = getFractionsArray(query);
+    float[] quantiles = null;
+
+    float[] values = getValuesArray(query);
+    String resultType = getResultType(query);
+    double[] ranks = null;
+
+    // since we know REQ is coming
+    switch (type) {
+      case KLL:
+        KllFloatsSketch kll = (KllFloatsSketch) sketch;
+        isEstimationMode = kll.isEstimationMode();
+        maxValue = kll.getMaxValue();
+        minValue = kll.getMinValue();
+        streamLength = kll.getN();
+
+        if (values != null) {
+          ranks = resultType.equals(QUERY_RESULT_TYPE_CDF) ? kll.getCDF(values) : kll.getPMF(values);
+        }
+
+        if (fractions != null) {
+          quantiles = kll.getQuantiles(fractions);
+        }
+
+        if (addSummary)
+          summary = kll.toString();
+        break;
+
+      default:
+        throw new SketchesException("processQuantilesQuery() received a non-quantiles sketch: " + type.toString());
+    }
+
+    JsonObject result = new JsonObject();
+    result.addProperty(RESPONSE_STREAM_LENGTH, streamLength);
+    result.addProperty(RESPONSE_ESTIMATION_MODE_FIELD, isEstimationMode);
+    result.addProperty(RESPONSE_MIN_VALUE, minValue);
+    result.addProperty(RESPONSE_MAX_VALUE, maxValue);
+
+    if (ranks != null) {
+      final String label = resultType.equals(QUERY_RESULT_TYPE_PMF) ? RESPONSE_RESULT_MASS : RESPONSE_RESULT_RANK;
+      JsonArray rankArray = new JsonArray();
+      for (int i = 0; i < ranks.length; ++i) {
+        JsonObject rankPair = new JsonObject();
+        if (i == values.length) {
+          rankPair.addProperty(RESPONSE_RESULT_VALUE, maxValue);
+        } else {
+          rankPair.addProperty(RESPONSE_RESULT_VALUE, values[i]);
+        }
+        rankPair.addProperty(label, ranks[i]);
+        rankArray.add(rankPair);
+      }
+
+      if (resultType.equals(QUERY_RESULT_TYPE_CDF))
+        result.add(RESPONSE_CDF_LIST, rankArray);
+      else
+        result.add(RESPONSE_PMF_LIST, rankArray);
+    }
+
+    if (quantiles != null) {
+      JsonArray quantileArray = new JsonArray();
+      for (int i = 0; i < quantiles.length; ++i) {
+        JsonObject quantilePair = new JsonObject();
+        quantilePair.addProperty(RESPONSE_RESULT_RANK, fractions[i]);
+        quantilePair.addProperty(RESPONSE_RESULT_QUANTILE, quantiles[i]);
+        quantileArray.add(quantilePair);
+      }
+      result.add(RESPONSE_QUANTILE_LIST, quantileArray);
+    }
+
+    if (addSummary)
+      result.addProperty(RESPONSE_SUMMARY_FIELD, summary);
+
+    return result;
+  }
+
+  // only one sketch type here so could use ItemsSketch<String> instead of Object, but
+  // we'll eep the signatures generic here
+  @SuppressWarnings("unchecked")
+  private JsonObject processFrequencyQuery(JsonObject query, Family type, Object sketch) {
+    if (query == null || type != Family.FREQUENCY || sketch == null) {
+      return null;
+    }
+
+    ItemsSketch<String> sk = (ItemsSketch<String>) sketch;
+
+    // check if we need a summary
+    boolean addSummary = checkSummaryFlag(query);
+
+    if (!query.has(QUERY_ERRORTYPE_FIELD)) {
+      throw new SketchesException("Must specify a value for " + QUERY_ERRORTYPE_FIELD
+          + " for Frequent Items queries");
+    }
+
+    ErrorType errorType;
+    String errorTypeString = query.get(QUERY_ERRORTYPE_FIELD).getAsString();
+    if (errorTypeString.equals(QUERY_ERRORTYPE_NO_FP)) {
+      errorType = ErrorType.NO_FALSE_POSITIVES;
+    } else if (errorTypeString.equals(QUERY_ERRORTYPE_NO_FN)) {
+      errorType = ErrorType.NO_FALSE_NEGATIVES;
+    } else {
+      throw new SketchesException("Unknown Frequent Items ErrorType: " + errorTypeString);
+    }
+
+    ItemsSketch.Row<String>[] items = sk.getFrequentItems(errorType);
+
+    JsonArray itemArray = new JsonArray();
+    for (ItemsSketch.Row<String> item : items) {
+      JsonObject row = new JsonObject();
+      row.addProperty(RESPONSE_ITEM_VALUE, item.getItem());
+      row.addProperty(RESPONSE_ITEM_ESTIMATE, item.getEstimate());
+      row.addProperty(RESPONSE_ITEM_UPPER_BOUND, item.getUpperBound());
+      row.addProperty(RESPONSE_ITEM_LOWER_BOUND, item.getLowerBound());
+      itemArray.add(row);
+    }
+
+    JsonObject result = new JsonObject();
+    result.add(RESPONSE_ITEMS_ARRAY, itemArray);
+    if (addSummary)
+      result.addProperty(RESPONSE_SUMMARY_FIELD, sk.toString());
+
+    return result;
+  }
+
+  @SuppressWarnings("unchecked")
+  private JsonObject processSamplingQuery(JsonObject query, Family type, Object sketch) {
+    if (query == null || type == null || sketch == null) {
+      return null;
+    }
+
+    // check if we need a summary
+    boolean addSummary = checkSummaryFlag(query);
+    String summary = null;
+
+    long streamWeight;
+    int k;
+    JsonArray itemArray = new JsonArray();
+
+    switch (type) {
+      case RESERVOIR:
+        ReservoirItemsSketch<String> ris = (ReservoirItemsSketch<String>) sketch;
+        for (String item : ris.getSamples()) {
+          itemArray.add(item);
+        }
+        streamWeight = ris.getN();
+        k = ris.getK();
+        if (addSummary)
+          summary = ris.toString();
+        break;
+
+      case VAROPT:
+        VarOptItemsSketch<String> vis = (VarOptItemsSketch<String>) sketch;
+        for (VarOptItemsSamples<String>.WeightedSample ws : vis.getSketchSamples()) {
+          JsonObject item = new JsonObject();
+          item.addProperty(RESPONSE_ITEM_VALUE, ws.getItem());
+          item.addProperty(RESPONSE_ITEM_WEIGHT, ws.getWeight());
+          itemArray.add(item);
+        }
+        streamWeight = vis.getN();
+        k = vis.getK();
+        if (addSummary)
+          summary = vis.toString();
+        break;
+
+      default:
+        throw new SketchesException("processSamplingQuery() received a non-sampling sketch: " + type.toString());
+    }
+
+    JsonObject result = new JsonObject();
+    result.addProperty(RESPONSE_SKETCH_K, k);
+    result.addProperty(RESPONSE_STREAM_WEIGHT, streamWeight);
+    result.add(RESPONSE_ITEMS_ARRAY, itemArray);
+    if (addSummary)
+      result.addProperty(RESPONSE_SUMMARY_FIELD, summary);
+
+    return result;
+  }
+
+  // returns an array of rank points, or null if none in query
+  private float[] getValuesArray(JsonObject query) {
+    if (query == null || !query.has(QUERY_VALUES_FIELD_NAME)) {
+      return null;
+    }
+
+    JsonArray valuesArray = query.get(QUERY_VALUES_FIELD_NAME).getAsJsonArray();
+    float[] values = new float[valuesArray.size()];
+
+    for (int i = 0; i < values.length; ++i) {
+      if (!valuesArray.get(i).isJsonPrimitive()) {
+        throw new SketchesException("Invalid value in array. Must be a floating point value, found: " + valuesArray.get(i));
+      }
+      values[i] = valuesArray.get(i).getAsFloat();
+    }
+
+    return values;
+  }
+
+  // returns QUERY_RESULT_TYPE_PMF if specified in QUERY_RESULT_TYPE_NAME_FIELD, otherwise returns default of
+  // QUERY_RESULT_TYPE_CDF
+  private String getResultType(JsonObject query) {
+    if (query == null || !query.has(QUERY_RESULT_TYPE_NAME_FIELD)) {
+      return QUERY_RESULT_TYPE_CDF;
+    }
+
+    JsonElement resultTypeElement = query.get(QUERY_RESULT_TYPE_NAME_FIELD);
+    if (!resultTypeElement.isJsonPrimitive())
+      return QUERY_RESULT_TYPE_CDF;
+
+    String resultType = resultTypeElement.getAsString().toLowerCase();
+    if (resultType.equals(QUERY_RESULT_TYPE_PMF))
+      return QUERY_RESULT_TYPE_PMF;
+    else
+      return QUERY_RESULT_TYPE_CDF;
+  }
+
+  // returns an array of provided split points, or null if none in query
+  private double[] getFractionsArray(JsonObject query) {
+    if (query == null || !query.has(QUERY_FRACTIONS_NAME_FIELD)) {
+      return null;
+    }
+
+    JsonArray fractionsArray = query.get(QUERY_FRACTIONS_NAME_FIELD).getAsJsonArray();
+    double[] fractions = new double[fractionsArray.size()];
+
+    for (int i = 0; i < fractions.length; ++i) {
+      if (!fractionsArray.get(i).isJsonPrimitive()) {
+        throw new SketchesException("Invalid value in array. Must be float in [0.0, 1.0], found: " + fractionsArray.get(i));
+      }
+      fractions[i] = fractionsArray.get(i).getAsFloat();
+    }
+
+    return fractions;
+  }
+
+}
diff --git a/src/main/java/org/apache/datasketches/server/MergeHandler.java b/src/main/java/org/apache/datasketches/server/MergeHandler.java
new file mode 100644
index 0000000..ecc07e1
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/MergeHandler.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashSet;
+
+import org.apache.datasketches.ArrayOfStringsSerDe;
+import org.apache.datasketches.Family;
+import org.apache.datasketches.SketchesException;
+import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.cpc.CpcUnion;
+import org.apache.datasketches.frequencies.ItemsSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.memory.Memory;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsUnion;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsUnion;
+import org.apache.datasketches.theta.CompactSketch;
+import org.apache.datasketches.theta.SetOperationBuilder;
+import org.apache.datasketches.theta.Union;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * Performs a merge operation between sketches.
+ * <pre>
+ *   {
+ *     "target": "&lt;destination_key&gt;", // optional: return serialized result if missing
+ *     "source": [ { "family": "&lt;sketch_family&gt;",
+ *                "data": "&lt;base64_encoded_sketch&gt;"
+ *              } ]
+ *   }
+ * </pre>
+ * where <tt>source</tt> is an array of key names or {family, data} pairs. Inputs must be of the same family
+ * as the target. If no target is present, the family of the first input sketch is used instead. Merge order
+ * is not guaranteed.
+ */
+public class MergeHandler extends BaseSketchesQueryHandler {
+  MergeHandler(SketchStorage sketches) {
+    super(sketches);
+  }
+
+  @Override
+  protected JsonObject processQuery(JsonObject query) {
+    // optional targets:
+    // If no QUERY_MERGE_TGT_FIELD serialize the result, but then need specify QUERY_MERGE_K_FIELD.
+    // If a valid target is present, any value of QUERY_MERGE_K_FIELD is ignored
+    JsonElement dstElement = query.get(SketchConstants.QUERY_MERGE_TGT_FIELD);
+    String dst = dstElement != null ? dstElement.getAsString() : null;
+    if (dst != null && !sketches.contains(dst)) {
+      throw new IllegalArgumentException("Specified target sketch does not exist: " + dst);
+    }
+
+    int k = 0;
+    if (dst == null) {
+      JsonElement kElement = query.get(SketchConstants.QUERY_MERGE_K_FIELD);
+      if (kElement == null) {
+        throw new IllegalArgumentException("Must specify either \"" + SketchConstants.QUERY_MERGE_TGT_FIELD
+            + "\" or \"" + SketchConstants.QUERY_MERGE_K_FIELD + "\". Neither found.");
+      }
+      k = kElement.getAsInt();
+    }
+
+    JsonElement srcElement = query.get(SketchConstants.QUERY_MERGE_SRC_FIELD);
+    if (srcElement == null || !srcElement.isJsonArray()) {
+      throw new IllegalArgumentException("Merge source data must be a JSON Array");
+    }
+    JsonArray srcList = srcElement.getAsJsonArray();
+
+    SketchStorage.SketchEntry se = null;
+    Family dstFamily = null;
+    if (dst != null) {
+      se = sketches.getSketch(dst);
+      dstFamily = se.family;
+    }
+
+    // we'll process (and dedup) any stored sketches before we handle encoded inputs
+    // but we'll run through all of them before doing anything
+    ArrayList<Object> srcSketches = new ArrayList<>(srcList.size());
+
+    dstFamily = prepareSketches(srcList, dstFamily, dst, srcSketches);
+    byte[] skBytes = mergeSketches(dstFamily, k, se, srcSketches);
+
+    // skBytes == null if merging into another sketch; only non-null if returning a serialized image
+    if (skBytes != null) {
+      JsonObject result = new JsonObject();
+      result.addProperty(SketchConstants.QUERY_ENCODING_FIELD, SketchConstants.ENCODING_TYPE);
+      result.addProperty(SketchConstants.QUERY_SKETCH_FIELD, Base64.getEncoder().encodeToString(skBytes));
+      return result;
+    } else {
+      return null;
+    }
+  }
+
+  private Family prepareSketches(JsonArray sources, Family family, String dst, ArrayList<Object> sketchList) {
+    HashSet<String> namedSet = new HashSet<>();
+
+    // TODO: Check for sketch value types with distinct counting?
+    //       Less obvious how to handle serialized sketch input in that case
+
+    // add destination if one exists just in case it's also listed as a source
+    if (dst != null) {
+      namedSet.add(dst);
+    }
+
+    for (JsonElement elmt : sources) {
+      if (elmt.isJsonPrimitive()) {
+        // check family
+        String key = elmt.getAsString();
+        SketchStorage.SketchEntry entry = sketches.getSketch(key);
+        if (entry == null || (family != null && family != entry.family)) {
+          throw new SketchesException("Input sketches must exist and be of the same family as the target");
+        }
+
+        // add to set, save family if we didn't have one yet
+        if (!namedSet.contains(key)) {
+          namedSet.add(key);
+          if (family == null) {
+            family = entry.family;
+          }
+
+          // if we have a theta Union we need to get the result first
+          if (entry.family == Family.UNION) {
+            sketchList.add(((Union) entry.sketch).getResult());
+          } else {
+            sketchList.add(entry.sketch);
+          }
+        }
+      } else { // is JsonObject
+        // need special handling for theta as we store Unions?
+        JsonObject sourceObj = elmt.getAsJsonObject();
+        if (!sourceObj.has(SketchConstants.QUERY_FAMILY_FIELD)
+            || !sourceObj.has(SketchConstants.QUERY_DATA_FIELD)) {
+          throw new SketchesException("Base64 sketch used as merge input must specify both \""
+              + SketchConstants.QUERY_FAMILY_FIELD + "\" and \"" + SketchConstants.QUERY_DATA_FIELD + "\"");
+        }
+
+        Family skFamily = familyFromString(sourceObj.get(SketchConstants.QUERY_FAMILY_FIELD).getAsString());
+        String skString = sourceObj.get(SketchConstants.QUERY_DATA_FIELD).getAsString();
+        if (skString == null || (family != null && family != skFamily)) {
+          throw new SketchesException("Input sketches must exist and be of the same family as the target");
+        }
+
+        // add to list, save family if we didn't have one yet
+        Object sketch = deserializeSketch(skFamily, skString);
+        sketchList.add(sketch);
+        if (family == null) {
+          family = skFamily;
+        }
+      }
+    }
+
+    return family;
+  }
+
+  private Object deserializeSketch(Family family, String b64String) {
+    if (family == null || b64String == null) {
+      return null;
+    }
+
+    Memory skBytes = Memory.wrap(Base64.getDecoder().decode(b64String));
+
+    switch (family) {
+      case QUICKSELECT:
+        return CompactSketch.heapify(skBytes);
+
+      case HLL:
+        return HllSketch.heapify(skBytes);
+
+      case CPC:
+        return CpcSketch.heapify(skBytes);
+
+      case KLL:
+        return KllFloatsSketch.heapify(skBytes);
+
+      case FREQUENCY:
+        return ItemsSketch.getInstance(skBytes, new ArrayOfStringsSerDe());
+
+      case RESERVOIR:
+        return ReservoirItemsSketch.heapify(skBytes, new ArrayOfStringsSerDe());
+
+      case VAROPT:
+        return VarOptItemsSketch.heapify(skBytes, new ArrayOfStringsSerDe());
+
+      default:
+        throw new SketchesException("Unsupported sketch family: " + family.toString());
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private byte[] mergeSketches(Family family, int k, SketchStorage.SketchEntry dstEntry, ArrayList<Object> sketchList) {
+    if (family == null || sketchList.size() == 0) {
+      return null;
+    }
+
+    switch (family) {
+      case UNION:
+      case QUICKSELECT: {
+        // for HLL, the destination is already a union so no need to add explicitly
+        Union dst = dstEntry == null ? new SetOperationBuilder().setNominalEntries(1 << k).buildUnion()
+            : (Union) dstEntry.sketch;
+        for (Object obj : sketchList) {
+          dst.update((CompactSketch) obj);
+        }
+
+        if (dstEntry == null) {
+          return dst.getResult().toByteArray();
+        } else {
+          dstEntry.sketch = dst;
+          return null;
+        }
+      }
+
+      case HLL: {
+        org.apache.datasketches.hll.Union union = new org.apache.datasketches.hll.Union(k);
+        if (dstEntry != null) {
+          union.update((HllSketch) dstEntry.sketch);
+        }
+        for (Object obj : sketchList) {
+          union.update((HllSketch) obj);
+        }
+
+        if (dstEntry == null) {
+          return union.getResult().toCompactByteArray();
+        } else {
+          dstEntry.sketch = union.getResult();
+          return null;
+        }
+      }
+
+      case CPC: {
+        CpcUnion union = new CpcUnion(k);
+        if (dstEntry != null) {
+          union.update((CpcSketch) dstEntry.sketch);
+        }
+        for (Object obj : sketchList) {
+          union.update((CpcSketch) obj);
+        }
+
+        if (dstEntry == null) {
+          return union.getResult().toByteArray();
+        } else {
+          dstEntry.sketch = union.getResult();
+          return null;
+        }
+      }
+
+      case KLL: {
+        // Only merge(), no separate union. Slightly abusing terminology to call it union
+        KllFloatsSketch union = dstEntry == null ? new KllFloatsSketch(k) : (KllFloatsSketch) dstEntry.sketch;
+
+        for (Object obj : sketchList) {
+          union.merge((KllFloatsSketch) obj);
+        }
+
+        if (dstEntry == null) {
+          return union.toByteArray();
+        } else {
+          dstEntry.sketch = union;
+          return null;
+        }
+      }
+
+      case FREQUENCY: {
+        // Only merge(), no separate union. Slightly abusing terminology to call it union
+        ItemsSketch<String> union = dstEntry == null ? new ItemsSketch<>(k) : (ItemsSketch<String>) dstEntry.sketch;
+
+        for (Object obj : sketchList) {
+          union.merge((ItemsSketch<String>) obj);
+        }
+
+        if (dstEntry == null) {
+          return union.toByteArray(new ArrayOfStringsSerDe());
+        } else {
+          dstEntry.sketch = union;
+          return null;
+        }
+      }
+
+      case RESERVOIR: {
+        ReservoirItemsUnion<String> union = ReservoirItemsUnion.newInstance(k);
+        if (dstEntry != null) {
+          union.update((ReservoirItemsSketch<String>) dstEntry.sketch);
+        }
+
+        for (Object obj : sketchList) {
+          union.update((ReservoirItemsSketch<String>) obj);
+        }
+
+        if (dstEntry == null) {
+         return union.getResult().toByteArray(new ArrayOfStringsSerDe());
+        } else {
+          dstEntry.sketch = union.getResult();
+          return null;
+        }
+      }
+
+      case VAROPT: {
+        VarOptItemsUnion<String> union = VarOptItemsUnion.newInstance(k);
+        if (dstEntry != null) {
+          union.update((VarOptItemsSketch<String>) dstEntry.sketch);
+        }
+
+        for (Object obj : sketchList) {
+          union.update((VarOptItemsSketch<String>) obj);
+        }
+
+        if (dstEntry == null) {
+          return union.getResult().toByteArray(new ArrayOfStringsSerDe());
+        } else {
+          dstEntry.sketch = union.getResult();
+          return null;
+        }
+      }
+
+      default:
+        throw new SketchesException("Unsupported sketch family: " + family.toString());
+    }
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/SerializationHandler.java b/src/main/java/org/apache/datasketches/server/SerializationHandler.java
new file mode 100644
index 0000000..be78b4c
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/SerializationHandler.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.util.Base64;
+
+import org.apache.datasketches.ArrayOfStringsSerDe;
+import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.frequencies.ItemsSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.Union;
+
+import com.google.gson.JsonObject;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ * Returns a serialized image of a sketch, encoded in base64, as well as the sketch family and, if relevant,
+ * the <tt>ValueType</tt> used.
+ * <pre>
+ *   {
+ *     "name": "&lt;sketch_name&gt;"
+ *   }
+ * </pre>
+ */
+public class SerializationHandler extends BaseSketchesQueryHandler {
+  public SerializationHandler(SketchStorage sketches) {
+    super(sketches, false);
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  protected JsonObject processQuery(JsonObject query) {
+    if (!query.has(QUERY_NAME_FIELD)) {
+      throw new IllegalArgumentException("Query missing sketch name field");
+    }
+
+    String name = query.get(QUERY_NAME_FIELD).getAsString();
+    if (!sketches.contains(name)) {
+      throw new IllegalArgumentException("Invalid sketch name: " + name);
+    }
+
+    SketchStorage.SketchEntry se = sketches.getSketch(name);
+
+    byte[] bytes;
+    switch (se.family) {
+      case UNION:
+        bytes = ((Union) se.sketch).getResult().toByteArray();
+        break;
+      case KLL:
+        bytes = ((KllFloatsSketch) se.sketch).toByteArray();
+        break;
+      case FREQUENCY:
+        bytes = ((ItemsSketch<String>) se.sketch).toByteArray(new ArrayOfStringsSerDe());
+        break;
+      case HLL:
+        bytes = ((HllSketch) se.sketch).toCompactByteArray();
+        break;
+      case CPC:
+        bytes = ((CpcSketch) se.sketch).toByteArray();
+        break;
+      case RESERVOIR:
+        bytes = ((ReservoirItemsSketch<String>) se.sketch).toByteArray(new ArrayOfStringsSerDe());
+        break;
+      case VAROPT:
+        bytes = ((VarOptItemsSketch<String>) se.sketch).toByteArray(new ArrayOfStringsSerDe());
+        break;
+      default:
+        throw new IllegalStateException("Unexpected value: " + se.family);
+    }
+
+    String b64Sketch = Base64.getEncoder().encodeToString(bytes);
+
+    JsonObject result = new JsonObject();
+    result.addProperty(QUERY_NAME_FIELD, name);
+    result.addProperty(CONFIG_FAMILY_FIELD, se.family.getFamilyName());
+    if (se.type != null)
+      result.addProperty(CONFIG_TYPE_FIELD, se.type.getTypeName());
+    result.addProperty(QUERY_ENCODING_FIELD, ENCODING_TYPE);
+    result.addProperty(QUERY_SKETCH_FIELD, b64Sketch);
+
+    return result;
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/SketchConstants.java b/src/main/java/org/apache/datasketches/server/SketchConstants.java
new file mode 100644
index 0000000..dc04a2f
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/SketchConstants.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+public final class SketchConstants {
+  // API call paths, relative to root
+  public static final String UPDATE_PATH = "/update";
+  public static final String SERIALIZE_PATH = "/serialize";
+  public static final String STATUS_PATH = "/status";
+  public static final String QUERY_PATH = "/query";
+  public static final String MERGE_PATH = "/merge";
+
+  // JSON Query/Update/Merge Field Names
+  public static final String QUERY_NAME_FIELD = "name";
+  public static final String QUERY_FAMILY_FIELD = "family";
+  public static final String QUERY_ENCODING_FIELD = "encoding";
+  public static final String QUERY_SKETCH_FIELD = "sketch";
+  public static final String QUERY_DATA_FIELD = "data";
+  public static final String QUERY_PAIR_ITEM_FIELD = "item";
+  public static final String QUERY_PAIR_WEIGHT_FIELD = "weight";
+  public static final String QUERY_MERGE_TGT_FIELD = "target";
+  public static final String QUERY_MERGE_SRC_FIELD = "source";
+  public static final String QUERY_MERGE_K_FIELD = "k";
+  public static final String QUERY_SUMMARY_FIELD = "summary";
+  public static final String QUERY_ERRORTYPE_FIELD = "errorType";
+  public static final String QUERY_ERRORTYPE_NO_FP = "noFalsePositives";
+  public static final String QUERY_ERRORTYPE_NO_FN = "noFalseNegatives";
+  public static final String QUERY_VALUES_FIELD_NAME = "values";
+  public static final String QUERY_FRACTIONS_NAME_FIELD = "fractions";
+  public static final String QUERY_RESULT_TYPE_NAME_FIELD = "resultType";
+  public static final String QUERY_RESULT_TYPE_PMF = "pmf";
+  public static final String QUERY_RESULT_TYPE_CDF = "cdf";
+
+  // JSON Query Response Field Names
+  public static final String RESPONSE_SUMMARY_FIELD = QUERY_SUMMARY_FIELD;
+  public static final String RESPONSE_ESTIMATE_FIELD = "estimate";
+  public static final String RESPONSE_ESTIMATION_MODE_FIELD = "estimationMode";
+  public static final String RESPONSE_P1STDEV_FIELD = "plus1StdDev";
+  public static final String RESPONSE_P2STDEV_FIELD = "plus2StdDev";
+  public static final String RESPONSE_P3STDEV_FIELD = "plus3StdDev";
+  public static final String RESPONSE_M1STDEV_FIELD = "minus1StdDev";
+  public static final String RESPONSE_M2STDEV_FIELD = "minus2StdDev";
+  public static final String RESPONSE_M3STDEV_FIELD = "minus3StdDev";
+  public static final String RESPONSE_ITEMS_ARRAY = "items";
+  public static final String RESPONSE_ITEM_VALUE = "item";
+  public static final String RESPONSE_ITEM_ESTIMATE = "estimate";
+  public static final String RESPONSE_ITEM_UPPER_BOUND = "upperBound";
+  public static final String RESPONSE_ITEM_LOWER_BOUND = "lowerBound";
+  public static final String RESPONSE_STREAM_WEIGHT = "streamWeight"; // used for sampling
+  public static final String RESPONSE_SKETCH_K = "sketchK";
+  public static final String RESPONSE_ITEM_WEIGHT = "weight";
+  public static final String RESPONSE_STREAM_LENGTH = "streamLength";
+  public static final String RESPONSE_MAX_VALUE = "maxValue";
+  public static final String RESPONSE_MIN_VALUE = "minValue";
+  public static final String RESPONSE_CDF_LIST = "estimatedCDF";
+  public static final String RESPONSE_PMF_LIST = "estimatedPMF";
+  public static final String RESPONSE_RESULT_VALUE = "value";
+  public static final String RESPONSE_RESULT_RANK = "rank";
+  public static final String RESPONSE_RESULT_MASS = "mass";
+  public static final String RESPONSE_QUANTILE_LIST = "estimatedQuantiles";
+  public static final String RESPONSE_RESULT_QUANTILE = "quantile";
+
+  // JSON Config Field Names
+  public static final String CONFIG_PORT_FIELD = "port";
+  public static final String CONFIG_SKETCHES_PREFIX = "sketches"; // >= 1 fully described sketches
+  public static final String CONFIG_SET_PREFIX = "set";
+  public static final String CONFIG_K_FIELD = "k";
+  public static final String CONFIG_FAMILY_FIELD = "family";
+  public static final String CONFIG_TYPE_FIELD = "type"; // value type, only for distinct counting
+  public static final String CONFIG_SKETCH_NAME_FIELD = "name";
+  public static final String CONFIG_SET_NAMES_FIELD = "names";
+
+  // JSON Sketch Types
+  public static final String SKETCH_FAMILY_THETA = "theta";
+  public static final String SKETCH_FAMILY_HLL = "hll";
+  public static final String SKETCH_FAMILY_CPC = "cpc";
+  public static final String SKETCH_FAMILY_FREQUENCY = "frequency";
+  public static final String SKETCH_FAMILY_KLL = "kll";
+  public static final String SKETCH_FAMILY_RESERVOIR = "reservoir";
+  public static final String SKETCH_FAMILY_VAROPT = "varopt";
+
+  // JSON Value Types (applicable only for distinct counting)
+  public static final String VALUE_TYPE_INT = "int";
+  public static final String VALUE_TYPE_LONG = "long";
+  public static final String VALUE_TYPE_FLOAT = "float";
+  public static final String VALUE_TYPE_DOUBLE = "double";
+  public static final String VALUE_TYPE_STRING = "string";
+
+  // server configuration
+  public static final int DEFAULT_PORT = 8080;
+
+  // response codes
+  public static final int UNPROCESSABLE_ENTITY = 422; // defined, but not in HttpServletResponse.SC_* codes
+
+  public static final String ERROR_KEY = "error";
+
+  public static final String ENCODING_TYPE = "base64";
+}
diff --git a/src/main/java/org/apache/datasketches/server/SketchServer.java b/src/main/java/org/apache/datasketches/server/SketchServer.java
new file mode 100644
index 0000000..eed38c4
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/SketchServer.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+
+/**
+ * Creates a very basic sketch server running embedded Jetty. Configuration options are specified in a config
+ * file; for details @see SketchServerConfig.
+ */
+public class SketchServer {
+  private final SketchServerConfig config;
+  private SketchStorage sketches;
+  private Server server;
+
+  /**
+   * Creates a server with the provided configuration
+   * @param configFile Path to a configuration file following the <tt>SketchServerConfig</tt> format
+   * @throws IOException
+   */
+  public SketchServer(final String configFile) throws IOException {
+    config = new SketchServerConfig(configFile);
+  }
+
+  // defines paths and registers the relevant handlers
+  private void createServer() {
+    server = new Server(config.getPort());
+
+    // Error page unless you have a correct URL
+    ContextHandler contextRoot = new ContextHandler("/");
+    contextRoot.setContextPath("/");
+    contextRoot.setHandler(new ErrorHandler());
+
+    ContextHandler contextStatus = new ContextHandler(SketchConstants.STATUS_PATH);
+    contextStatus.setHandler(new StatusHandler(sketches));
+
+    ContextHandler contextSerialize = new ContextHandler(SketchConstants.SERIALIZE_PATH);
+    contextSerialize.setHandler(new SerializationHandler(sketches));
+
+    ContextHandler contextUpdate = new ContextHandler(SketchConstants.UPDATE_PATH);
+    contextUpdate.setHandler(new UpdateHandler(sketches));
+
+    ContextHandler contextMerge = new ContextHandler(SketchConstants.MERGE_PATH);
+    contextMerge.setHandler(new MergeHandler(sketches));
+
+    ContextHandler contextQuery = new ContextHandler(SketchConstants.QUERY_PATH);
+    contextQuery.setHandler(new DataQueryHandler(sketches));
+
+    ContextHandlerCollection contexts =
+        new ContextHandlerCollection(contextRoot,
+            contextStatus,
+            contextSerialize,
+            contextUpdate,
+            contextMerge,
+            contextQuery);
+    server.setHandler(contexts);
+  }
+
+  /**
+   * Initializes the sketches, configures query handlers, and Starts the server.
+   * @throws Exception
+   */
+  public void start() throws Exception {
+    sketches = new SketchStorage(config.getSketchList());
+    createServer();
+    server.start();
+  }
+
+  /**
+   * Returns the server's base URI
+   * @return Server URI as a string
+   */
+  public String getURI() {
+    if (server == null)
+      return null;
+    return server.getURI().toString();
+  }
+
+  /**
+   * Returns the server's configured port
+   * @return The server's pert
+   */
+  public int getPort() {
+    if (server != null) {
+      return ((ServerConnector) server.getConnectors()[0]).getLocalPort();
+    }
+    return -1;
+  }
+
+  public static void main(String[] args) throws Exception {
+    //String confFile = args[0];
+    String confFile = "conf3.json";
+
+    SketchServer sketchServer = new SketchServer(confFile);
+    sketchServer.start();
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/SketchServerConfig.java b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
new file mode 100644
index 0000000..8a1a4c5
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import static org.apache.datasketches.server.SketchStorage.isDistinctCounting;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ * A class to hold the server configuration, along with a supporting subclass and file-parsing methods.
+ *
+ * TODO: define config options
+ */
+class SketchServerConfig {
+  public static class SketchInfo {
+    public String name;
+    public int k;
+    public String family;
+    public String type;
+
+    SketchInfo(String name, int k, String family, String type) {
+      this.name = name;
+      this.k = k;
+      this.family = family;
+      this.type = type;
+    }
+
+    SketchInfo(String name, int k, String family) {
+      this.name = name;
+      this.k = k;
+      this.family = family;
+      this.type = null;
+    }
+
+  }
+
+  private int port = SketchConstants.DEFAULT_PORT;
+  private ArrayList<SketchInfo> sketchList;
+
+  SketchServerConfig(String configFile) throws IOException {
+    JsonElement config = readJsonFromFile(configFile);
+    parseConfig(config);
+  }
+
+  SketchServerConfig(JsonElement config) throws IOException {
+    parseConfig(config);
+  }
+
+  int getPort() {
+    return port;
+  }
+
+  List<SketchInfo> getSketchList() {
+    return sketchList;
+  }
+
+  // output should have a list with full info per sketch, even if input allows a
+  // more condensed format
+  private JsonElement readJsonFromFile(String configFile) {
+    JsonElement config = null;
+
+    try {
+      Reader reader = Files.newBufferedReader(Paths.get(configFile));
+      config = JsonParser.parseReader(reader);
+      reader.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+
+    return config;
+  }
+
+  private void parseConfig(JsonElement config) throws IOException {
+    Gson gson = new Gson();
+
+    sketchList = new ArrayList<>();
+
+    if (config.isJsonArray()) {
+      // must be a list of fully-described sketches
+      sketchList.addAll(Arrays.asList(gson.fromJson(config.getAsJsonArray(), SketchInfo[].class)));
+    } else if (config.isJsonObject()) {
+      JsonObject confEntry = config.getAsJsonObject();
+      for (String name : confEntry.keySet()) {
+        if (name.toLowerCase().equals(CONFIG_PORT_FIELD)) {
+          // port the server should use
+          port = confEntry.get(name).getAsInt();
+        }
+        else if (name.toLowerCase().startsWith(CONFIG_SKETCHES_PREFIX)) {
+          // sketches* is an array of fully qualified sketches
+          sketchList.addAll(Arrays.asList(gson.fromJson(confEntry.get(name).getAsJsonArray(), SketchInfo[].class)));
+        } else if (name.toLowerCase().startsWith(CONFIG_SET_PREFIX)) {
+          // set* has a common name and type with an array of name names
+          JsonObject sketchSetInfo = confEntry.get(name).getAsJsonObject();
+          int k = sketchSetInfo.get(CONFIG_K_FIELD).getAsInt();
+          String family = sketchSetInfo.get(CONFIG_FAMILY_FIELD).getAsString();
+          String type = null;
+          if (isDistinctCounting(BaseSketchesQueryHandler.familyFromString(family))) {
+            type = sketchSetInfo.get(CONFIG_TYPE_FIELD).getAsString();
+          }
+          String[] nameList = gson.fromJson(sketchSetInfo.get(CONFIG_SET_NAMES_FIELD).getAsJsonArray(), String[].class);
+
+          for (String n : nameList)
+            sketchList.add(new SketchInfo(n, k, family, type));
+        }
+      }
+    } else {
+      throw new IOException("Expected JsonArray or JsonObject but none found");
+    }
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/SketchStorage.java b/src/main/java/org/apache/datasketches/server/SketchStorage.java
new file mode 100644
index 0000000..dd4b1ed
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/SketchStorage.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.datasketches.Family;
+import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.frequencies.ItemsSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.SetOperationBuilder;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ * A storage class for holding sketches. Each sketch must have a unique name, and be instantiated with one
+ * of the known types. For some sketch families, a <tt>ValueType</tt> (@see ValueType) may be required in
+ * order to ensure that data is presented in a consistent way.
+ */
+public class SketchStorage {
+  private static final String SKETCH_COUNT_NAME = "count";
+
+  /**
+   * Returns true if the sketch family is for distinct counting.
+   * @param family A sketch Family (@see org.apache.datasketches.Family)
+   * @return <tt>true</tt>e for distinct counting sketch families, otherwise <tt>false</tt>>.
+   */
+  static boolean isDistinctCounting(final Family family) {
+    return family == Family.QUICKSELECT || family == Family.UNION || family == Family.HLL || family == Family.CPC;
+  }
+
+  static class SketchEntry {
+    public final Family family;
+    public final ValueType type;
+    public Object sketch;
+
+    SketchEntry(Family family, ValueType type, Object sketch) throws IllegalArgumentException {
+      if (isDistinctCounting(family) && type == null)
+        throw new IllegalArgumentException("Must specify a value type for distinct counting sketches");
+
+      this.family = family;
+      this.type = type;
+      this.sketch = sketch;
+    }
+
+    SketchEntry(Family family, Object sketch) throws IllegalArgumentException {
+      if (isDistinctCounting(family))
+        throw new IllegalArgumentException("Must specify a value type for distinct counting sketches");
+
+      this.family = family;
+      this.type = null;
+      this.sketch = sketch;
+    }
+  }
+
+  HashMap<String, SketchEntry> sketchMap;
+
+  SketchStorage(List<SketchServerConfig.SketchInfo> sketchList) {
+    if (sketchList != null) {
+      createSketches(sketchList);
+    }
+  }
+
+  JsonObject listSketches() {
+    JsonObject summary = new JsonObject();
+
+    JsonArray sketchList = new JsonArray(sketchMap.size());
+    for (Map.Entry<String, SketchEntry> e : sketchMap.entrySet()) {
+      JsonObject item = new JsonObject();
+      item.addProperty(CONFIG_SKETCH_NAME_FIELD, e.getKey());
+      switch (e.getValue().family) {
+        case UNION:
+          item.addProperty(CONFIG_TYPE_FIELD, e.getValue().type.getTypeName());
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_THETA);
+          break;
+        case CPC:
+          item.addProperty(CONFIG_TYPE_FIELD, e.getValue().type.getTypeName());
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_CPC);
+          break;
+        case HLL:
+          item.addProperty(CONFIG_TYPE_FIELD, e.getValue().type.getTypeName());
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_HLL);
+          break;
+        case FREQUENCY:
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_FREQUENCY);
+          break;
+        case KLL:
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_KLL);
+          break;
+        case RESERVOIR:
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_RESERVOIR);
+          break;
+        case VAROPT:
+          item.addProperty(CONFIG_FAMILY_FIELD, SKETCH_FAMILY_VAROPT);
+          break;
+      }
+      sketchList.add(item);
+    }
+
+    summary.addProperty(SKETCH_COUNT_NAME, sketchMap.size());
+    summary.add(SketchConstants.CONFIG_SKETCHES_PREFIX, sketchList); // bare prefix, sketches fully qualified
+
+    return summary;
+  }
+
+  boolean contains(String key) {
+     return sketchMap.containsKey(key);
+  }
+
+  SketchEntry getSketch(String key) {
+    return sketchMap.get(key);
+  }
+
+  // instantiate the actual sketches, throwing if there's a duplicate key
+  private void createSketches(List<SketchServerConfig.SketchInfo> list) throws IllegalArgumentException {
+    sketchMap = new HashMap<>(list.size());
+
+    for (SketchServerConfig.SketchInfo info : list) {
+      if (sketchMap.containsKey(info.name)) {
+        throw new IllegalArgumentException("Duplicate sketch key: " + info.name);
+      }
+
+      SketchEntry sketchEntry = null;
+      Family family = BaseSketchesQueryHandler.familyFromString(info.family);
+
+      switch (family) {
+        case QUICKSELECT:
+          // make a Union so we can handle merges later
+          sketchEntry = new SketchEntry(Family.UNION, ValueType.stringToType(info.type),
+              new SetOperationBuilder().setNominalEntries(1 << info.k).buildUnion());
+          break;
+
+        case HLL:
+          sketchEntry = new SketchEntry(Family.HLL, ValueType.stringToType(info.type),
+              new HllSketch(info.k));
+          break;
+
+        case CPC:
+          sketchEntry = new SketchEntry(Family.CPC, ValueType.stringToType(info.type),
+              new CpcSketch(info.k));
+          break;
+
+        case KLL:
+          sketchEntry = new SketchEntry(Family.KLL, new KllFloatsSketch(info.k));
+          break;
+
+        case FREQUENCY:
+          sketchEntry = new SketchEntry(Family.FREQUENCY, new ItemsSketch<String>(info.k));
+          break;
+
+        case RESERVOIR:
+          sketchEntry = new SketchEntry(Family.RESERVOIR, ReservoirItemsSketch.<String>newInstance(info.k));
+          break;
+
+        case VAROPT:
+          sketchEntry = new SketchEntry(Family.VAROPT, VarOptItemsSketch.<String>newInstance(info.k));
+          break;
+      }
+
+      if (sketchEntry != null) {
+        sketchMap.put(info.name, sketchEntry);
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/StatusHandler.java b/src/main/java/org/apache/datasketches/server/StatusHandler.java
new file mode 100644
index 0000000..d782a12
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/StatusHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Returns a JSON list of sketches held by the server and their types.
+ */
+public class StatusHandler extends BaseSketchesQueryHandler {
+
+  public StatusHandler(SketchStorage sketches) {
+    super(sketches, true);
+  }
+
+  @Override
+  protected JsonObject processQuery(JsonObject query) {
+    return sketches.listSketches();
+  }
+}
diff --git a/src/main/java/org/apache/datasketches/server/UpdateHandler.java b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
new file mode 100644
index 0000000..2cc362f
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.util.Map;
+
+import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.frequencies.ItemsSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.Union;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import static org.apache.datasketches.server.SketchConstants.*;
+
+/**
+ *
+ * <p>
+ * Update format (single- and multi-value updates):
+ * <pre>
+ * {
+ *   "&lt;sketch_name_1&gt;": &lt;value&gt;,
+ *   "&lt;sketch_name_2&gt;": [&lt;value_1&gt;, &lt;value_2&gt;, ...].
+ *   ...
+ * }
+ * </pre>
+ * where the type of each value is determined by the sketch configuration at server initialization.
+ * </p>
+ * <p>
+ * Each <em>value</em> may be a stand-alone item or, for sketches accepting weighted inputs, may be a JSON
+ * object: <tt>{ "item": &lt;value&gt;, "weight": &lt;weight&gt; }</tt>. Attempting to use a weight with a
+ * sketch that does not support weighted inputs will result in an error. The multi-value update format
+ * may contain a mux of weighted and unweighted items, as lonig s the sketch accepts weighted values.
+ * </p>
+ * <p>
+ * The JSON parsing library does not allow duplicate key names; to send multiple values into the same sketch
+ * in a single request, the multi-value option <em>must</em> be used.
+ * </p>
+ * <p>
+ * This is not a transactional database: Entries are processed and committed sequentially, returning an error
+ * if one is encountered at any point in the process. Any updates processed prior to the error will be
+ * retained by the server.
+ * </p>
+ */
+public class UpdateHandler extends BaseSketchesQueryHandler {
+  public UpdateHandler(SketchStorage sketches) {
+    super(sketches);
+  }
+
+  @Override
+  protected JsonObject processQuery(JsonObject query) {
+    for (Map.Entry<String, JsonElement> entry : query.entrySet()) {
+      String name = entry.getKey();
+      SketchStorage.SketchEntry se = sketches.getSketch(name);
+      JsonElement data = entry.getValue();
+
+      if (name == null || se == null || data == null) {
+        throw new IllegalArgumentException("Attempt to call update with missing name or sketch not found");
+      }
+
+      if (data.isJsonArray()) {
+        processBatchUpdate(se, data.getAsJsonArray());
+      } else {
+        processSingleUpdate(se, data);
+      }
+    }
+
+    // nothing to return from update
+    return null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void processBatchUpdate(SketchStorage.SketchEntry entry,
+                                  JsonArray data) {
+    switch (entry.family) {
+      case UNION: // theta
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            for (JsonElement e : data) { ((Union) entry.sketch).update(e.getAsDouble()); }
+            break;
+          case INT: case LONG:
+            for (JsonElement e : data) { ((Union) entry.sketch).update(e.getAsLong()); }
+            break;
+          case STRING: default:
+            for (JsonElement e : data) { ((Union) entry.sketch).update(e.getAsString()); }
+            break;
+        }
+        break;
+
+      case CPC:
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            for (JsonElement e : data) { ((CpcSketch) entry.sketch).update(e.getAsDouble()); }
+            break;
+          case INT: case LONG:
+            for (JsonElement e : data) { ((CpcSketch) entry.sketch).update(e.getAsLong()); }
+            break;
+          case STRING: default:
+            for (JsonElement e : data) { ((CpcSketch) entry.sketch).update(e.getAsString()); }
+            break;
+        }
+        break;
+
+      case HLL:
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            for (JsonElement e : data) { ((HllSketch) entry.sketch).update(e.getAsDouble()); }
+            break;
+          case INT: case LONG:
+            for (JsonElement e : data) { ((CpcSketch) entry.sketch).update(e.getAsLong()); }
+            break;
+          case STRING: default:
+            for (JsonElement e : data) { ((CpcSketch) entry.sketch).update(e.getAsString()); }
+            break;
+        }
+        break;
+
+      case KLL:
+        for (JsonElement e : data) { ((KllFloatsSketch) entry.sketch).update(e.getAsFloat()); }
+        break;
+
+      case FREQUENCY:
+        for (JsonElement e : data) {
+          if (e.isJsonObject()) {
+            JsonObject inputPair = e.getAsJsonObject();
+            if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+              throw new IllegalArgumentException("Frequent Items input pairs must include both "
+                  + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+            }
+            String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+            int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+            ((ItemsSketch<String>) entry.sketch).update(item, weight);
+          } else {
+            ((ItemsSketch<String>) entry.sketch).update(e.getAsString());
+          }
+        }
+        break;
+
+      case RESERVOIR:
+        for (JsonElement e : data) { ((ReservoirItemsSketch<String>) entry.sketch).update(e.getAsString()); }
+        break;
+
+      case VAROPT:
+        for (JsonElement e : data) {
+          if (e.isJsonObject()) {
+            JsonObject inputPair = e.getAsJsonObject();
+            if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+              throw new IllegalArgumentException("VarOpt input pairs must include both "
+                  + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+            }
+            String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+            double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
+            ((VarOptItemsSketch<String>) entry.sketch).update(item, weight);
+          } else {
+            ((VarOptItemsSketch<String>) entry.sketch).update(e.getAsString(), 1.0);
+          }
+        }
+        break;
+
+      default:
+        throw new IllegalArgumentException("Unsupported sketch type: " + entry.family);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private void processSingleUpdate(SketchStorage.SketchEntry entry,
+                                   JsonElement data) {
+    switch (entry.family) {
+      case UNION:
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            ((Union) entry.sketch).update(data.getAsDouble());
+            break;
+          case INT: case LONG:
+            ((Union) entry.sketch).update(data.getAsLong());
+            break;
+          case STRING: default:
+            ((Union) entry.sketch).update(data.getAsString());
+            break;
+        }
+        break;
+
+      case CPC:
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            ((CpcSketch) entry.sketch).update(data.getAsDouble());
+            break;
+          case INT: case LONG:
+            ((CpcSketch) entry.sketch).update(data.getAsLong());
+            break;
+          case STRING: default:
+            ((CpcSketch) entry.sketch).update(data.getAsString());
+            break;
+        }
+        break;
+
+      case HLL:
+        assert(entry.type != null);
+        switch (entry.type) {
+          case FLOAT: case DOUBLE:
+            ((HllSketch) entry.sketch).update(data.getAsDouble());
+            break;
+          case INT: case LONG:
+            ((HllSketch) entry.sketch).update(data.getAsLong());
+            break;
+          case STRING: default:
+            ((HllSketch) entry.sketch).update(data.getAsString());
+            break;
+        }
+        break;
+
+      case KLL:
+        ((KllFloatsSketch) entry.sketch).update(data.getAsFloat());
+        break;
+
+      case FREQUENCY:
+        if (data.isJsonObject()) {
+          JsonObject inputPair = data.getAsJsonObject();
+          if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+            throw new IllegalArgumentException("Frequent Items input pairs must include both "
+                + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+          }
+          String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+          int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+          ((ItemsSketch<String>) entry.sketch).update(item, weight);
+        } else {
+          ((ItemsSketch<String>) entry.sketch).update(data.getAsString());
+        }
+        break;
+
+      case RESERVOIR:
+        ((ReservoirItemsSketch<String>) entry.sketch).update(data.getAsString());
+        break;
+
+      case VAROPT:
+        if (data.isJsonObject()) {
+          JsonObject inputPair = data.getAsJsonObject();
+          if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+            throw new IllegalArgumentException("VarOpt input pairs must include both "
+                + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+          }
+          String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+          double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
+          ((VarOptItemsSketch<String>) entry.sketch).update(item, weight);
+        } else {
+          ((VarOptItemsSketch<String>) entry.sketch).update(data.getAsString(), 1.0);
+        }
+        break;
+
+      default:
+        throw new IllegalArgumentException("Unsupported sketch type: " + entry.family);
+    }
+  }
+
+}
+
diff --git a/src/main/java/org/apache/datasketches/server/ValueType.java b/src/main/java/org/apache/datasketches/server/ValueType.java
new file mode 100644
index 0000000..0ec1ff9
--- /dev/null
+++ b/src/main/java/org/apache/datasketches/server/ValueType.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.datasketches.server;
+
+import java.util.HashMap;
+
+/**
+ * An enum to hold the known data value types.
+ */
+public enum ValueType {
+
+  /**
+   * Character string. Uses default encoding.
+   */
+  STRING(SketchConstants.VALUE_TYPE_STRING),
+
+  /**
+   * 32-bit floating point value
+   */
+  FLOAT(SketchConstants.VALUE_TYPE_FLOAT),
+
+  /**
+   * 64-bit floating point value
+   */
+  DOUBLE(SketchConstants.VALUE_TYPE_DOUBLE),
+
+  /**
+   * 32-bit signed integer value
+   */
+  INT(SketchConstants.VALUE_TYPE_INT),
+
+  /**
+   * 64-bit signed integer value
+   */
+  LONG(SketchConstants.VALUE_TYPE_LONG);
+
+
+  private static final HashMap<String, ValueType> lookupTypeName = new HashMap<>();
+  private final String typeName_;
+
+  static {
+    for (ValueType t : values()) {
+      lookupTypeName.put(t.getTypeName().toLowerCase(), t);
+    }
+  }
+
+  ValueType(final String typeName) {
+    typeName_ = typeName;
+  }
+
+  public String getTypeName() {
+    return typeName_;
+  }
+
+  /**
+   * Returns the ValueType given the type name
+   * @param typeName the family name
+   * @return the ValueType given the type name
+   */
+  public static ValueType stringToType(final String typeName) {
+    final ValueType t = lookupTypeName.get(typeName.toLowerCase());
+    if (t == null) {
+      throw new IllegalArgumentException("Illegal ValueType Name: " + typeName);
+    }
+    return t;
+  }
+
+
+}