diff --git a/Dockerfile b/Dockerfile
index a528c9d6e793fc6f7c31ddc68391cb8e243c5b3b..2912436f2bd8f35716d1c550a43061d7a811a829 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,6 +22,8 @@
 ARG project=nshmp-haz-v2
 ARG builder_workdir=/app/${project}
 ARG libs_dir=${builder_workdir}/build/libs
+ARG jar_file=${libs_dir}/${project}.jar
+ARG ws_file=${libs_dir}/${project}-ws.jar
 
 ####
 # Builder image: Build jar and war file.
@@ -31,6 +33,8 @@ FROM usgs/centos:8 as builder
 ENV LANG="en_US.UTF-8"
 
 ARG builder_workdir
+ARG libs_dir
+ARG ws_file
 
 WORKDIR ${builder_workdir}
 
@@ -39,7 +43,8 @@ COPY . .
 RUN yum install -y java-11-openjdk-devel which git
 
 RUN mv nshmp-lib ../. \
-    && ./gradlew --no-daemon assemble
+    && ./gradlew --no-daemon assemble \
+    && mv ${libs_dir}/*-all.jar ${ws_file}
 
 ####
 # Application image: Run jar or war file.
@@ -51,13 +56,13 @@ LABEL maintainer="Peter Powers <pmpowers@usgs.gov>, Brandon Clayton <bclayton@us
 ENV LANG="en_US.UTF-8"
 
 ARG libs_dir
+ARG ws_file
 ARG builder_workdir
 ARG project
-ARG TOMCAT_MAJOR=8
-ARG TOMCAT_VERSION=${TOMCAT_MAJOR}.5.40
 
 ENV PROJECT ${project}
-ENV JAVA_XMX 8g
+ENV CONTEXT_PATH "/"
+ENV MODEL_PATH /app/models
 
 # Whether to run hazard jar file or web services war file
 ENV RUN_HAZARD true
@@ -72,30 +77,13 @@ ENV IML ""
 ENV CONFIG_FILE "config.json"
 VOLUME [ "/app/output" ]
 
-# Tomcat
-ENV CONTEXT_PATH ""
-ENV CATALINA_HOME /usr/local/tomcat
-ENV TOMCAT_WEBAPPS ${CATALINA_HOME}/webapps
-ENV PATH ${CATALINA_HOME}/bin:${PATH}
-ENV TOMCAT_SOURCE http://archive.apache.org/dist/tomcat
-ENV TOMCAT_URL ${TOMCAT_SOURCE}/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz
-ENV JAVA_OPTS -Xmx${JAVA_XMX}
-
-ENV WS_HOME ${CATALINA_HOME}
-ENV HAZ_HOME /app
-
-WORKDIR ${HAZ_HOME}
+WORKDIR /app
 
 COPY --from=builder ${libs_dir}/* ./
 COPY docker-entrypoint.sh .
 
-WORKDIR ${WS_HOME}
-
 RUN yum update -y \
-    && yum install -y file jq zip java-11-openjdk-headless \
-    && curl -L ${TOMCAT_URL} | tar -xz --strip-components=1
-
-WORKDIR ${HAZ_HOME}
+    && yum install -y file jq zip java-11-openjdk-headless
 
 EXPOSE 8080
 ENTRYPOINT [ "bash", "docker-entrypoint.sh" ]
diff --git a/build.gradle b/build.gradle
index c18eead82cc949c081022f3f234040b14665f256..4a89efb7cfc6168eb289f0580c5f2e3532e1151c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -63,10 +63,6 @@ repositories {
 dependencies {
   implementation project(":nshmp-lib")
 
-  // Tomcat
-  implementation "org.apache.tomcat:tomcat-catalina:8.0.45"
-  implementation "javax.websocket:javax.websocket-api:1.1"
-
   // AWS
   implementation "com.amazonaws:aws-lambda-java-core:${awsLambdaCoreVersion}"
   implementation "com.amazonaws:aws-java-sdk-lambda:${awsLambdaVersion}"
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index b114c559ce870c7b7dd4250283bc7d78aab6bd89..45ee388fb68a2ea88c2edd5210d07267ab16e692 100644
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -107,7 +107,7 @@ run_hazard() {
 
   # Run nshmp-haz
   java -Xms${JAVA_XMS} -Xmx${JAVA_XMX} \
-      -cp ${HAZ_HOME}/${PROJECT}.jar \
+      -cp /app/${PROJECT}.jar \
       gov.usgs.earthquake.nshmp.${nshmp_program} \
       "${nshmp_model_path}" \
       "${site_file}" \
@@ -130,9 +130,15 @@ run_hazard() {
 #   None
 ####
 run_ws() {
-  unpack_war;
   get_ws_models;
-  catalina.sh run 2>&1;
+
+  if [ -z ${CONTEXT_PATH} ]; then
+    CONTEXT_PATH="/nshmp/${MODEL}";
+  fi
+
+  java -jar ${PROJECT}-ws.jar \
+      "-Dmicronaut.server.context-path=${CONTEXT_PATH}" \
+      -model=/app/models
 }
 
 ####
@@ -510,34 +516,6 @@ move_to_output_volume() {
   cp -r ${hazout}/* output/. 2> ${LOG_FILE};
 }
 
-####
-# Unpack war file into webapps/<CONTEXT_PATH>.
-# Globals:
-#   (string) CONTEXT_PATH - The context path for the web services
-#   (string) LOG_FILE - The log file
-#   (string) MODEL - The model to use
-#   (string) PROJECT - The project name
-#   (string) TOMCAT_WEBAPPS - The Tomcat webapps dir
-# Arguments:
-#   None
-# Returns:
-#   None
-####
-unpack_war() {
-  cd ${TOMCAT_WEBAPPS} 2> ${LOG_FILE};
-
-  if [ -z ${CONTEXT_PATH} ]; then
-    CONTEXT_PATH="nshmp/${MODEL}";
-  fi
-
-  CONTEXT_PATH=$(echo ${CONTEXT_PATH//\//#} | awk {'print tolower($0)'}) 2> ${LOG_FILE};
-  mkdir ${CONTEXT_PATH} 2> ${LOG_FILE};
-  cd ${CONTEXT_PATH} 2> ${LOG_FILE};
-  cp ${HAZ_HOME}/${PROJECT}.war . 2> ${LOG_FILE};
-  unzip ${PROJECT}.war 2> ${LOG_FILE} &> /dev/null;
-  rm ${PROJECT}.war 2> ${LOG_FILE};
-}
-
 ####
 # Run main
 ####
diff --git a/scripts/nshmp-haz.yml b/scripts/nshmp-haz.yml
index 24a997ef5fc1dd49bcf57f08a260685a2fb9c697..e2282207a31365ef41e93959bf8898186a094a00 100644
--- a/scripts/nshmp-haz.yml
+++ b/scripts/nshmp-haz.yml
@@ -23,7 +23,7 @@ services:
     environment:
       RUN_HAZARD: 'false'
       MODEL: CONUS-2018
-      CONTEXT_PATH: nshmp/conus-2018
+      CONTEXT_PATH: /nshmp/conus-2018
 
   # # Deploy nshmp-haz with CONUS-2014
   # nshmp-haz-conus-2014:
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java
index 64ab2cb6625785d0c2124447856b1c6d2c19c058..813b121651f06815913be42513002691d97ad99d 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultSliceLambda.java
@@ -3,7 +3,7 @@ package gov.usgs.earthquake.nshmp.aws;
 import static com.google.common.base.Preconditions.checkState;
 import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE;
 import static gov.usgs.earthquake.nshmp.aws.Util.MAP_FILE;
-import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON;
+import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
@@ -37,8 +37,8 @@ import gov.usgs.earthquake.nshmp.data.Interpolator;
 import gov.usgs.earthquake.nshmp.internal.Parsing;
 import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil;
 
 /**
  * AWS Lambda function to read in a curves file from AWS S3 and create slices at
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java
index e6738a36e29958979f61fe2212a450c1766e816b..d470a01809d33cdabae28a1959075c318eb60cb4 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsMetadataLambda.java
@@ -2,7 +2,7 @@ package gov.usgs.earthquake.nshmp.aws;
 
 import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE;
 import static gov.usgs.earthquake.nshmp.aws.Util.MAP_FILE;
-import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON;
+import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -36,8 +36,8 @@ import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.internal.Parsing;
 import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil;
 
 /**
  * AWS Lambda function to list all hazard results in the nshmp-hazout S3 bucket
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java
index 9480ce954b4ce4d975d81783b9573a76268ea060..8f6f6c6df6358d96c06997980c5d0c4fb0cbe0af 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/aws/HazardResultsSlicerLambda.java
@@ -1,7 +1,7 @@
 package gov.usgs.earthquake.nshmp.aws;
 
 import static gov.usgs.earthquake.nshmp.aws.Util.CURVES_FILE;
-import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON;
+import static gov.usgs.earthquake.nshmp.www.services.ServletUtil.GSON;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -34,8 +34,8 @@ import gov.usgs.earthquake.nshmp.aws.Util.LambdaHelper;
 import gov.usgs.earthquake.nshmp.internal.Parsing;
 import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil;
 
 /**
  * AWS Lambda function to read in hazard results from S3 and to create slices of
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java
new file mode 100644
index 0000000000000000000000000000000000000000..df7472f331b7c56e392bd60ce07b0a22d8774c21
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonController.java
@@ -0,0 +1,118 @@
+package gov.usgs.earthquake.nshmp.www;
+
+import java.util.EnumMap;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet;
+import gov.usgs.earthquake.nshmp.www.services.DeaggEpsilonService;
+import gov.usgs.earthquake.nshmp.www.services.DeaggEpsilonService.Query;
+import gov.usgs.earthquake.nshmp.www.services.HazardService;
+
+import io.micronaut.context.event.StartupEvent;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.QueryValue;
+import io.micronaut.runtime.event.annotation.EventListener;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "Epsilon Deaggregation Service (experimental)")
+@Controller("/deagg-epsilon")
+public class DeaggEpsilonController {
+
+  @Inject
+  private NshmpMicronautServlet servlet;
+
+  @EventListener
+  public void init(StartupEvent event) {
+    DeaggEpsilonService.init();
+  }
+
+  @Get(uri = "/usage", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGetUsage(HttpRequest<?> request) {
+    var urlHelper = servlet.urlHelper(request);
+    return HazardService.handleDoGetUsage(urlHelper);
+  }
+
+  /**
+   * GET method to return usage or hazard curves, query based.
+   * 
+   * @param request The HTTP request
+   * @param longitude Longitude (in decimal degrees) ([-360, 360])
+   * @param latitude Latitude (in decimal degrees) ([-90, 90])
+   * @param vs30 The site soil class
+   * @param basin Whether to use basin service
+   */
+  @Operation(
+      summary = "Compute epsilon deaggregation",
+      description = "Compute epsilon deaggregation given longitude, latitude, Vs30 and IMT-IML map",
+      operationId = "deaggEpsilon_doGetDeaggEpsilon")
+  @ApiResponse(
+      description = "Epsilon deaggregations",
+      responseCode = "200")
+  @Get(uri = "{?longitude,latitude,vs30,basin}")
+  public HttpResponse<String> doGetDeaggEpsilon(
+      HttpRequest<?> request,
+      @Schema(
+          required = true,
+          minimum = "-360",
+          maximum = "360") @QueryValue @Nullable Double longitude,
+      @Schema(
+          required = true,
+          minimum = "-90",
+          maximum = "90") @QueryValue @Nullable Double latitude,
+      @Schema(required = true) @QueryValue @Nullable Integer vs30,
+      @Schema(defaultValue = "false") @QueryValue @Nullable Boolean basin,
+      @Schema(
+          defaultValue = "{\"SA0P01\": 0.01}",
+          required = true) @QueryValue @Nullable EnumMap<Imt, Double> imtImls) {
+    var urlHelper = servlet.urlHelper(request);
+    var query = new Query(request, longitude, latitude, vs30, basin);
+    return DeaggEpsilonService.handleDoGetDeaggEpsilon(query, urlHelper);
+  }
+
+  /**
+   * GET method to return usage or hazard curves, slash based.
+   * 
+   * @param request The HTTP request
+   * @param longitude Longitude (in decimal degrees) ([-360, 360])
+   * @param latitude Latitude (in decimal degrees) ([-90, 90])
+   * @param vs30 The site soil class
+   * @param basin Whether to use basin service
+   */
+  @Operation(
+      summary = "Compute epsilon deaggregation",
+      description = "Compute epsilon deaggregation given longitude, latitude, Vs30 and IMT-IML map",
+      operationId = "deaggEpsilon_doGetDeaggEpsilonSlash")
+  @ApiResponse(
+      description = "Epsilon deaggregations",
+      responseCode = "200")
+  @Get(uri = "{/longitude}{/latitude}{/vs30}{/basin}")
+  public HttpResponse<String> doGetDeaggEpsilonSlash(
+      HttpRequest<?> request,
+      @Schema(
+          required = true,
+          minimum = "-360",
+          maximum = "360") @PathVariable @Nullable Double longitude,
+      @Schema(
+          required = true,
+          minimum = "-90",
+          maximum = "90") @PathVariable @Nullable Double latitude,
+      @Schema(required = true) @PathVariable @Nullable Integer vs30,
+      @Schema(defaultValue = "false") @PathVariable @Nullable Boolean basin,
+      @Schema(
+          defaultValue = "{\"SA0P01\": 0.01}",
+          required = true) @QueryValue @Nullable EnumMap<Imt, Double> imtImls) {
+    return doGetDeaggEpsilon(request, longitude, latitude, vs30, basin, null);
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java
deleted file mode 100644
index ae17b6301861e795ec88d6aeb76fd37cfdf262ed..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/DeaggEpsilonService.java
+++ /dev/null
@@ -1,355 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static gov.usgs.earthquake.nshmp.www.ServletUtil.GSON;
-import static gov.usgs.earthquake.nshmp.www.ServletUtil.emptyRequest;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.BASIN;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.LATITUDE;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.LONGITUDE;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.MODEL;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.Key.VS30;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.readBoolean;
-import static gov.usgs.earthquake.nshmp.www.WsUtil.readDouble;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableList;
-
-import gov.usgs.earthquake.nshmp.calc.CalcConfig;
-import gov.usgs.earthquake.nshmp.calc.Deaggregation;
-import gov.usgs.earthquake.nshmp.calc.Hazard;
-import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
-import gov.usgs.earthquake.nshmp.calc.Site;
-import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
-import gov.usgs.earthquake.nshmp.geo.Location;
-import gov.usgs.earthquake.nshmp.gmm.Imt;
-import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.TimedTaskContext;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
-import gov.usgs.earthquake.nshmp.www.meta.Metadata;
-import gov.usgs.earthquake.nshmp.www.services.SourceServices;
-
-/**
- * Hazard deaggregation service.
- *
- * @author U.S. Geological Survey
- */
-@SuppressWarnings("unused")
-@WebServlet(
-    name = "Epsilon Deaggregation Service (experimental)",
-    description = "USGS NSHMP Hazard Deaggregator",
-    urlPatterns = { "/deagg-epsilon" })
-public final class DeaggEpsilonService extends NshmpServlet {
-
-  /* Developer notes: See HazardService. */
-
-  private LoadingCache<Model, HazardModel> modelCache;
-  private URL basinUrl;
-
-  private static final String USAGE = SourceServices.GSON.toJson(
-      new SourceServices.ResponseData());
-
-  @Override
-  @SuppressWarnings("unchecked")
-  public void init() throws ServletException {
-
-    ServletContext context = getServletConfig().getServletContext();
-    Object modelCache = context.getAttribute("");
-    this.modelCache = (LoadingCache<Model, HazardModel>) modelCache;
-
-    try (InputStream config =
-        DeaggEpsilonService.class.getResourceAsStream("/config.properties")) {
-
-      checkNotNull(config, "Missing config.properties");
-
-      Properties props = new Properties();
-      props.load(config);
-      if (props.containsKey("basin_host")) {
-        /*
-         * TODO Site builder tests if service is working, which may be
-         * inefficient for single call services.
-         */
-        URL url = new URL(props.getProperty("basin_host") + "/nshmp-site-ws/basin/local-data");
-        this.basinUrl = url;
-      }
-    } catch (IOException | NullPointerException e) {
-      throw new ServletException(e);
-    }
-  }
-
-  @Override
-  protected void doGet(
-      HttpServletRequest request,
-      HttpServletResponse response)
-      throws ServletException, IOException {
-
-    UrlHelper urlHelper = urlHelper(request, response);
-
-    if (emptyRequest(request)) {
-      urlHelper.writeResponse(USAGE);
-      return;
-    }
-
-    try {
-      RequestData requestData = buildRequestData(request);
-
-      /* Submit as task to job executor */
-      Deagg2Task task = new Deagg2Task(urlHelper.url, getServletContext(), requestData);
-      Result result = ServletUtil.TASK_EXECUTOR.submit(task).get();
-      GSON.toJson(result, response.getWriter());
-
-    } catch (Exception e) {
-      String message = Metadata.errorMessage(urlHelper.url, e, false);
-      response.getWriter().print(message);
-      getServletContext().log(urlHelper.url, e);
-    }
-  }
-
-  /* Reduce query string key-value pairs. */
-  static RequestData buildRequestData(HttpServletRequest request) {
-
-    try {
-
-      /* process query '?' request */
-      List<Model> models = readModelsFromQuery(request);
-      double lon = readDouble(LONGITUDE, request);
-      double lat = readDouble(LATITUDE, request);
-      Map<Imt, Double> imtImls = readImtsFromQuery(request);
-      double vs30 = readDouble(VS30, request);
-      boolean basin = readBoolean(BASIN, request);
-
-      return new RequestData(
-          models,
-          lon,
-          lat,
-          imtImls,
-          vs30,
-          basin);
-
-    } catch (Exception e) {
-      throw new IllegalArgumentException("Error parsing request URL", e);
-    }
-  }
-
-  private static List<Model> readModelsFromQuery(HttpServletRequest request) {
-    String[] ids = WsUtil.readValues(MODEL, request);
-    return Arrays.stream(ids)
-        .map(Model::valueOf)
-        .distinct()
-        .collect(ImmutableList.toImmutableList());
-  }
-
-  /* Create map of IMT to deagg IML. */
-  private static Map<Imt, Double> readImtsFromQuery(HttpServletRequest request) {
-    EnumMap<Imt, Double> imtImls = new EnumMap<>(Imt.class);
-    for (Entry<String, String[]> param : request.getParameterMap().entrySet()) {
-      if (isImtParam(param.getKey())) {
-        imtImls.put(
-            Imt.valueOf(param.getKey()),
-            Double.valueOf(param.getValue()[0]));
-      }
-    }
-    return imtImls;
-  }
-
-  private static boolean isImtParam(String key) {
-    return key.equals("PGA") || key.startsWith("SA");
-  }
-
-  private class Deagg2Task extends TimedTaskContext<Result> {
-
-    RequestData data;
-
-    Deagg2Task(String url, ServletContext context, RequestData data) {
-      super(url, context);
-      this.data = data;
-    }
-
-    @Override
-    public Result calc() throws Exception {
-      Deaggregation deagg = calcDeagg(data);
-
-      return new Result.Builder()
-          .requestData(data)
-          .url(url)
-          .timer(timer)
-          .deagg(deagg)
-          .build();
-    }
-  }
-
-  /*
-   * Developer notes:
-   *
-   * We're opting here to fetch basin terms ourselves. If we were to set the
-   * basin provider in the config, which requires additions to config, the URL
-   * is tested every time a site is created for a servlet request. While this
-   * worked for maps it's not good here.
-   *
-   * Site has logic for parsing the basin service response, which perhaps it
-   * shouldn't. TODO is it worth decomposing data objects and services
-   */
-  Deaggregation calcDeagg(RequestData data) {
-    Location loc = Location.create(data.latitude, data.longitude);
-
-    Site site = Site.builder()
-        .location(Location.create(data.latitude, data.longitude))
-        .basinDataProvider(data.basin ? this.basinUrl : null)
-        .vs30(data.vs30)
-        .build();
-
-    Hazard[] hazards = new Hazard[data.models.size()];
-    for (int i = 0; i < data.models.size(); i++) {
-      HazardModel model = modelCache.getUnchecked(data.models.get(i));
-      hazards[i] = process(model, site, data.imtImls.keySet());
-    }
-    Hazard hazard = Hazard.merge(hazards);
-    return Deaggregation.atImls(hazard, data.imtImls, ServletUtil.CALC_EXECUTOR);
-  }
-
-  private static Hazard process(HazardModel model, Site site, Set<Imt> imts) {
-    CalcConfig config = CalcConfig.copyOf(model.config())
-        .imts(imts)
-        .build();
-    // System.out.println(config);
-    return HazardCalcs.hazard(model, config, site, ServletUtil.CALC_EXECUTOR);
-  }
-
-  static final class RequestData {
-
-    final List<Model> models;
-    final double latitude;
-    final double longitude;
-    final Map<Imt, Double> imtImls;
-    final double vs30;
-    final boolean basin;
-
-    RequestData(
-        List<Model> models,
-        double longitude,
-        double latitude,
-        Map<Imt, Double> imtImls,
-        double vs30,
-        boolean basin) {
-
-      this.models = models;
-      this.latitude = latitude;
-      this.longitude = longitude;
-      this.imtImls = imtImls;
-      this.vs30 = vs30;
-      this.basin = basin;
-    }
-  }
-
-  private static final class ResponseData {
-
-    final List<Model> models;
-    final double longitude;
-    final double latitude;
-    final String imt;
-    final double iml;
-    final double vs30;
-    final String rlabel = "Closest Distance, rRup (km)";
-    final String mlabel = "Magnitude (Mw)";
-    final String εlabel = "% Contribution to Hazard";
-    final Object εbins;
-
-    ResponseData(Deaggregation deagg, RequestData request, Imt imt) {
-      this.models = request.models;
-      this.longitude = request.longitude;
-      this.latitude = request.latitude;
-      this.imt = imt.toString();
-      this.iml = request.imtImls.get(imt);
-      this.vs30 = request.vs30;
-      this.εbins = deagg.εBins();
-    }
-  }
-
-  private static final class Response {
-
-    final ResponseData metadata;
-    final Object data;
-
-    Response(ResponseData metadata, Object data) {
-      this.metadata = metadata;
-      this.data = data;
-    }
-  }
-
-  private static final class Result {
-
-    final String status = Status.SUCCESS.toString();
-    final String date = ZonedDateTime.now().format(ServletUtil.DATE_FMT);
-    final String url;
-    final Object server;
-    final List<Response> response;
-
-    Result(String url, Object server, List<Response> response) {
-      this.url = url;
-      this.server = server;
-      this.response = response;
-    }
-
-    static final class Builder {
-
-      String url;
-      Timer timer;
-      RequestData request;
-      Deaggregation deagg;
-
-      Builder deagg(Deaggregation deagg) {
-        this.deagg = deagg;
-        return this;
-      }
-
-      Builder url(String url) {
-        this.url = url;
-        return this;
-      }
-
-      Builder timer(Timer timer) {
-        this.timer = timer;
-        return this;
-      }
-
-      Builder requestData(RequestData request) {
-        this.request = request;
-        return this;
-      }
-
-      Result build() {
-        ImmutableList.Builder<Response> responseListBuilder = ImmutableList.builder();
-
-        for (Imt imt : request.imtImls.keySet()) {
-          ResponseData responseData = new ResponseData(deagg, request, imt);
-          Object deaggs = deagg.toJsonCompact(imt);
-          Response response = new Response(responseData, deaggs);
-          responseListBuilder.add(response);
-        }
-
-        List<Response> responseList = responseListBuilder.build();
-        Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer);
-
-        return new Result(url, server, responseList);
-      }
-    }
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java b/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java
deleted file mode 100644
index d81dee5cea2578f96965574c514324cf0b8659a6..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/NshmpServlet.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Custom NSHMP servlet implementation and URL helper class.
- *
- * <p>All nshmp-haz-v2 services should extend this class. This class sets custom
- * response headers and provides a helper class to ensure serialized response
- * URLs propagate the correct host and protocol from requests on USGS servers
- * and caches that may have been forwarded.
- *
- * <p>Class provides one convenience method,
- * {@code urlHelper.writeResponse(String)}, to write a servlet response wherein
- * any URL strings may be formatted with the correct protocol and host. Such URL
- * strings should start with:
- *
- * "%s://%s/service-name/..."
- *
- * @author U.S. Geological Survey
- */
-public abstract class NshmpServlet extends HttpServlet {
-
-  @Override
-  protected void service(
-      HttpServletRequest request,
-      HttpServletResponse response)
-      throws ServletException, IOException {
-
-    /*
-     * Set CORS headers and content type.
-     *
-     * Because nshmp-haz-v2 services may be called by both the USGS website,
-     * other websites, and directly by 3rd party applications, reponses
-     * generated by direct requests will not have the necessary header
-     * information that would be required by security protocols for web
-     * requests. This means that any initial direct request will pollute
-     * intermediate caches with a response that a browser will deem invalid.
-     */
-    response.setContentType("application/json; charset=UTF-8");
-    response.setHeader("Access-Control-Allow-Origin", "*");
-    response.setHeader("Access-Control-Allow-Methods", "*");
-    response.setHeader("Access-Control-Allow-Headers", "accept,origin,authorization,content-type");
-
-    super.service(request, response);
-  }
-
-  public static UrlHelper urlHelper(HttpServletRequest request, HttpServletResponse response)
-      throws IOException {
-    return new UrlHelper(request, response);
-  }
-
-  public static class UrlHelper {
-
-    private final HttpServletResponse response;
-    private final String urlPrefix;
-    public final String url;
-
-    UrlHelper(HttpServletRequest request, HttpServletResponse response) {
-
-      /*
-       * Check custom header for a forwarded protocol so generated links can use
-       * the same protocol and not cause mixed content errors.
-       */
-      String host = request.getServerName();
-      String protocol = request.getHeader("X-FORWARDED-PROTO");
-      String contextPath = request.getContextPath();
-      if (protocol == null) {
-        /* Not a forwarded request. Honor reported protocol and port. */
-        protocol = request.getScheme();
-        host += ":" + request.getServerPort();
-      }
-
-      /*
-       * For convenience, store a url field with the (possibly updated) request
-       * protocol and
-       */
-      StringBuffer urlBuf = request.getRequestURL();
-      String query = request.getQueryString();
-      if (query != null) urlBuf.append('?').append(query);
-      String url = urlBuf.toString().replace("http://", protocol + "://");
-
-      this.response = response;
-      this.urlPrefix = String.format("%s://%s%s", protocol, host, contextPath);
-      this.url = url;
-    }
-
-    /**
-     * Convenience method to update a string response with the correct protocol
-     * and host in URLs. URL strings should start with:
-     *
-     * "%s://%s/service-name/..."
-     */
-    public void writeResponse(String usage) throws IOException {
-      // TODO had to add duplicate fields to handle haz and g syntax strings
-      response.getWriter().printf(usage, urlPrefix, urlPrefix);
-    }
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java
deleted file mode 100644
index bf52c0e8b13e0c38a8a2f1941c0520d5268b5006..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/WsUtil.java
+++ /dev/null
@@ -1,213 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import javax.servlet.ServletRequest;
-
-import com.google.gson.GsonBuilder;
-
-import gov.usgs.earthquake.nshmp.internal.Parsing;
-import gov.usgs.earthquake.nshmp.internal.Parsing.Delimiter;
-import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
-import gov.usgs.earthquake.nshmp.internal.www.Response;
-import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
-import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
-
-import io.micronaut.http.HttpResponse;
-
-public class WsUtil {
-
-  public static HttpResponse<String> handleError(
-      Throwable e,
-      String name,
-      Logger logger,
-      UrlHelper urlHelper) {
-    var msg = e.getMessage() + " (see logs)";
-    var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper);
-    var gson = new GsonBuilder().setPrettyPrinting().create();
-    var response = gson.toJson(svcResponse);
-    logger.severe(name + " -\n" + response);
-    e.printStackTrace();
-    return HttpResponse.serverError(response);
-  }
-
-  public static interface ServiceQuery {
-    boolean isNull();
-
-    void checkValues();
-  }
-
-  public static class ServiceQueryData implements ServiceQuery {
-    public final Double longitude;
-    public final Double latitude;
-
-    public ServiceQueryData(Double longitude, Double latitude) {
-      this.longitude = longitude;
-      this.latitude = latitude;
-    }
-
-    @Override
-    public boolean isNull() {
-      return longitude == null && latitude == null;
-    }
-
-    @Override
-    public void checkValues() {
-      WsUtils.checkValue(Key.LONGITUDE, longitude);
-      WsUtils.checkValue(Key.LATITUDE, latitude);
-    }
-  }
-
-  public static class ServiceRequestData {
-    public final SourceModel model;
-    public final double longitude;
-    public final double latitude;
-
-    public ServiceRequestData(ServiceQueryData query) {
-      model = new SourceModel(ServletUtil.installedModel());
-      longitude = query.longitude;
-      latitude = query.latitude;
-    }
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a boolean.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   */
-  public static <E extends Enum<E>> boolean readBoolean(E key, ServletRequest request) {
-    return Boolean.valueOf(readValue(key, request));
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a double.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   */
-  public static <E extends Enum<E>> double readDouble(E key, ServletRequest request) {
-    return Double.valueOf(readValue(key, request));
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as an integer.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   */
-  public static <E extends Enum<E>> int readInteger(E key, ServletRequest request) {
-    return Integer.valueOf(readValue(key, request));
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a string.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   */
-  public static <E extends Enum<E>> String readValue(E key, ServletRequest request) {
-    return readValues(key, request)[0];
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a enum of specified
-   * type.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   * @param type of enum to return
-   */
-  public static <T extends Enum<T>, E extends Enum<E>> T readValue(
-      E key,
-      ServletRequest request,
-      Class<T> type) {
-    return Enum.valueOf(type, readValue(key, request));
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a string array.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   */
-  public static <E extends Enum<E>> String[] readValues(E key, ServletRequest request) {
-    return checkNotNull(
-        request.getParameterValues(key.toString()),
-        "Missing query key [" + key.toString() + "]");
-  }
-
-  /**
-   * Returns the value of a servlet request parameter as a enum set of specified
-   * type.
-   * 
-   * @param key of value to get
-   * @param request servlet request
-   * @param type of enum to return
-   */
-  public static <T extends Enum<T>, E extends Enum<E>> Set<T> readValues(
-      E key,
-      ServletRequest request,
-      Class<T> type) {
-
-    return Arrays.stream(readValues(key, request))
-        .map((name) -> Enum.valueOf(type, name))
-        .collect(Collectors.toSet());
-  }
-
-  public enum Key {
-    EDITION,
-    REGION,
-    MODEL,
-    VS30,
-    LATITUDE,
-    LONGITUDE,
-    IMT,
-    RETURNPERIOD,
-    DISTANCE,
-    FORMAT,
-    TIMESPAN,
-    BASIN;
-
-    private String label;
-
-    private Key() {
-      label = name().toLowerCase();
-    }
-
-    @Override
-    public String toString() {
-      return label;
-    }
-  }
-
-  static <T extends Enum<T>> Set<T> readValues(String values, Class<T> type) {
-    return Parsing.splitToList(values, Delimiter.COMMA).stream()
-        .map((name) -> Enum.valueOf(type, name))
-        .collect(Collectors.toSet());
-  }
-
-  static <E extends Enum<E>> String readValue(E key, Map<String, String[]> paramMap) {
-    String keyStr = key.toString();
-    String[] values = paramMap.get(keyStr);
-    checkNotNull(values, "Missing query key: %s", keyStr);
-    checkState(values.length > 0, "Empty value array for key: %s", key);
-    return values[0];
-  }
-
-  static <T extends Enum<T>, E extends Enum<E>> T readValue(
-      E key,
-      Map<String, String[]> paramMap,
-      Class<T> type) {
-    return Enum.valueOf(type, readValue(key, paramMap));
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java b/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java
deleted file mode 100644
index 697006fbfc5e34026bc462ee09c0cd70c74f8ca3..0000000000000000000000000000000000000000
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/XY_DataGroup.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package gov.usgs.earthquake.nshmp.www;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import gov.usgs.earthquake.nshmp.data.XySequence;
-
-/**
- * Container class of XY data sequences prior to Json serialization. This
- * implementation is for data series that share the same x-values
- *
- * @author U.S. Geological Survey
- */
-@SuppressWarnings("unused")
-@Deprecated
-public class XY_DataGroup {
-
-  private final String label;
-  private final String xLabel;
-  private final String yLabel;
-  protected final List<Series> data;
-
-  protected XY_DataGroup(String label, String xLabel, String yLabel) {
-    this.label = label;
-    this.xLabel = xLabel;
-    this.yLabel = yLabel;
-    this.data = new ArrayList<>();
-  }
-
-  /** Create a data group. */
-  public static XY_DataGroup create(String name, String xLabel, String yLabel) {
-    return new XY_DataGroup(name, xLabel, yLabel);
-  }
-
-  /** Add a data sequence */
-  public XY_DataGroup add(String id, String name, XySequence data) {
-    this.data.add(new Series(id, name, data));
-    return this;
-  }
-
-  static class Series {
-    private final String id;
-    private final String label;
-    private final XySequence data;
-
-    Series(String id, String label, XySequence data) {
-      this.id = id;
-      this.label = label;
-      this.data = data;
-    }
-  }
-
-}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
index 01f7370dcfbc95845aa92ef753780a6af43f4b47..992aa92c7267bc7e009e0079e4b07106098f83c4 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/meta/Metadata.java
@@ -6,8 +6,8 @@ import com.google.gson.annotations.SerializedName;
 import gov.usgs.earthquake.nshmp.geo.Coordinates;
 import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
 
 /**
  * Service metadata, parameterization, and constraint strings, in JSON format.
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java
new file mode 100644
index 0000000000000000000000000000000000000000..1036e20e12320af73e4830aa34ff4da55e72fef0
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/DeaggEpsilonService.java
@@ -0,0 +1,297 @@
+package gov.usgs.earthquake.nshmp.www.services;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.logging.Logger;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
+
+import gov.usgs.earthquake.nshmp.calc.CalcConfig;
+import gov.usgs.earthquake.nshmp.calc.Deaggregation;
+import gov.usgs.earthquake.nshmp.calc.Site;
+import gov.usgs.earthquake.nshmp.calc.Vs30;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
+import gov.usgs.earthquake.nshmp.internal.www.Response;
+import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
+import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
+import gov.usgs.earthquake.nshmp.www.BaseModel;
+import gov.usgs.earthquake.nshmp.www.DeaggEpsilonController;
+import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
+import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+
+/**
+ * Hazard deaggregation handler for {@link DeaggEpsilonController}.
+ *
+ * @author U.S. Geological Survey
+ */
+public final class DeaggEpsilonService {
+
+  /* Developer notes: See HazardService. */
+
+  private static final String NAME = "Epsilon Deaggregation";
+  private static final Logger LOGGER = Logger.getLogger(DeaggEpsilonService.class.getName());
+  private static URL basinUrl;
+
+  public static void init() {
+    try (InputStream config = Resources.getResource("config.properties").openStream()) {
+      checkNotNull(config, "Missing config.properties");
+
+      Properties props = new Properties();
+      props.load(config);
+      if (props.containsKey("basin_host")) {
+        /*
+         * TODO Site builder tests if service is working, which may be
+         * inefficient for single call services.
+         */
+        var url = new URL(props.getProperty("basin_host") + "/nshmp/ws/data/basin");
+        basinUrl = url;
+      }
+    } catch (IOException | NullPointerException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Handler for {@link DeaggEpsilonController#doGetDeaggEpsilon}. Returns the
+   * usage or the deagg result.
+   * 
+   * @param query The query
+   * @param urlHelper The URL helper
+   */
+  public static HttpResponse<String> handleDoGetDeaggEpsilon(Query query, UrlHelper urlHelper) {
+    try {
+      var timer = ServletUtil.timer();
+      LOGGER.info(NAME + " - Request:\n" + ServletUtil.GSON.toJson(query));
+
+      if (query.isNull()) {
+        return HazardService.handleDoGetUsage(urlHelper);
+      }
+
+      query.checkValues();
+      var data = new RequestData(query, Vs30.fromValue(query.vs30));
+      var response = process(data, timer, urlHelper);
+      var svcResponse = ServletUtil.GSON.toJson(response);
+      LOGGER.info(NAME + " - Response:\n" + svcResponse);
+      return HttpResponse.ok(svcResponse);
+    } catch (Exception e) {
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
+    }
+  }
+
+  /* Create map of IMT to deagg IML. */
+  private static EnumMap<Imt, Double> readImtsFromQuery(HttpRequest<?> request) {
+    var imtImls = new EnumMap<Imt, Double>(Imt.class);
+    for (var param : request.getParameters().asMap().entrySet()) {
+      if (isImtParam(param.getKey())) {
+        imtImls.put(
+            Imt.valueOf(param.getKey()),
+            Double.valueOf(param.getValue().get(0)));
+      }
+    }
+    return imtImls;
+  }
+
+  private static boolean isImtParam(String key) {
+    return key.equals("PGA") || key.startsWith("SA");
+  }
+
+  private static Response<RequestData, ResponseData> process(
+      RequestData data,
+      Timer timer,
+      UrlHelper urlHelper)
+      throws InterruptedException, ExecutionException {
+    var configFunction = new ConfigFunction();
+    var siteFunction = new SiteFunction(data);
+    var hazard = ServicesUtil.calcHazard(configFunction, siteFunction);
+    var deagg = Deaggregation.atImls(hazard, data.imtImls, ServletUtil.CALC_EXECUTOR);
+    return new ResultBuilder()
+        .deagg(deagg)
+        .requestData(data)
+        .timer(timer)
+        .urlHelper(urlHelper)
+        .build();
+  }
+
+  public static class Query extends HazardService.Query {
+    final EnumMap<Imt, Double> imtImls;
+    final Boolean basin;
+
+    public Query(
+        HttpRequest<?> request,
+        Double longitude,
+        Double latitude,
+        Integer vs30,
+        Boolean basin) {
+      super(longitude, latitude, vs30);
+      imtImls = readImtsFromQuery(request);
+      this.basin = basin == null ? false : basin;
+    }
+
+    @Override
+    public boolean isNull() {
+      return super.isNull() && vs30 == null;
+    }
+
+    @Override
+    public void checkValues() {
+      super.checkValues();
+      WsUtils.checkValue(Key.BASIN, basin);
+    }
+  }
+
+  static class ConfigFunction implements Function<BaseModel, CalcConfig> {
+    @Override
+    public CalcConfig apply(BaseModel baseModel) {
+      var hazardModel = ServletUtil.hazardModels().get(baseModel);
+      var configBuilder = CalcConfig.copyOf(hazardModel.config());
+      configBuilder.imts(baseModel.imts);
+      return configBuilder.build();
+    }
+  }
+
+  /*
+   * Developer notes:
+   *
+   * We're opting here to fetch basin terms ourselves. If we were to set the
+   * basin provider in the config, which requires additions to config, the URL
+   * is tested every time a site is created for a servlet request. While this
+   * worked for maps it's not good here.
+   *
+   * Site has logic for parsing the basin service response, which perhaps it
+   * shouldn't. TODO is it worth decomposing data objects and services
+   */
+  static class SiteFunction implements Function<CalcConfig, Site> {
+    final RequestData data;
+
+    private SiteFunction(RequestData data) {
+      this.data = data;
+    }
+
+    @Override
+    public Site apply(CalcConfig config) {
+      return Site.builder()
+          .location(Location.create(data.latitude, data.longitude))
+          .basinDataProvider(data.basin ? basinUrl : null)
+          .vs30(data.vs30.value())
+          .build();
+    }
+  }
+
+  static final class RequestData extends HazardService.RequestData {
+    final EnumMap<Imt, Double> imtImls;
+    final boolean basin;
+
+    RequestData(Query query, Vs30 vs30) {
+      super(query, vs30);
+      imtImls = query.imtImls;
+      basin = query.basin;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ResponseMetadata {
+    final SourceModel model;
+    final double longitude;
+    final double latitude;
+    final String imt;
+    final double iml;
+    final Vs30 vs30;
+    final String rlabel = "Closest Distance, rRup (km)";
+    final String mlabel = "Magnitude (Mw)";
+    final String εlabel = "% Contribution to Hazard";
+    final Object εbins;
+
+    ResponseMetadata(Deaggregation deagg, RequestData request, Imt imt) {
+      this.model = request.model;
+      this.longitude = request.longitude;
+      this.latitude = request.latitude;
+      this.imt = imt.toString();
+      this.iml = imt.period();
+      this.vs30 = request.vs30;
+      this.εbins = deagg.εBins();
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ResponseData {
+    final Object server;
+    final List<DeaggResponse> deaggs;
+
+    ResponseData(Object server, List<DeaggResponse> deaggs) {
+      this.server = server;
+      this.deaggs = deaggs;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class DeaggResponse {
+    final ResponseMetadata metadata;
+    final Object data;
+
+    DeaggResponse(ResponseMetadata metadata, Object data) {
+      this.metadata = metadata;
+      this.data = data;
+    }
+  }
+
+  static final class ResultBuilder {
+    UrlHelper urlHelper;
+    Timer timer;
+    RequestData request;
+    Deaggregation deagg;
+
+    ResultBuilder deagg(Deaggregation deagg) {
+      this.deagg = deagg;
+      return this;
+    }
+
+    ResultBuilder urlHelper(UrlHelper urlHelper) {
+      this.urlHelper = urlHelper;
+      return this;
+    }
+
+    ResultBuilder timer(Timer timer) {
+      this.timer = timer;
+      return this;
+    }
+
+    ResultBuilder requestData(RequestData request) {
+      this.request = request;
+      return this;
+    }
+
+    Response<RequestData, ResponseData> build() {
+      ImmutableList.Builder<DeaggResponse> responseListBuilder = ImmutableList.builder();
+
+      for (Imt imt : request.imtImls.keySet()) {
+        ResponseMetadata responseData = new ResponseMetadata(deagg, request, imt);
+        Object deaggs = deagg.toJsonCompact(imt);
+        DeaggResponse response = new DeaggResponse(responseData, deaggs);
+        responseListBuilder.add(response);
+      }
+
+      List<DeaggResponse> responseList = responseListBuilder.build();
+      Object server = Metadata.serverData(ServletUtil.THREAD_COUNT, timer);
+      var response = new ResponseData(server, responseList);
+
+      return new Response<>(Status.SUCCESS, NAME, request, response, urlHelper);
+    }
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
index 68bc040de6f1bb48898eb98dde59c06e2753304d..4039651427c294977b67c1a5ba36d224bdc90e52 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/HazardService.java
@@ -7,19 +7,16 @@ import java.util.ArrayList;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 import java.util.logging.Logger;
-import java.util.stream.Collectors;
 
 import gov.usgs.earthquake.nshmp.calc.CalcConfig;
 import gov.usgs.earthquake.nshmp.calc.Hazard;
-import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
 import gov.usgs.earthquake.nshmp.calc.Site;
 import gov.usgs.earthquake.nshmp.calc.Vs30;
 import gov.usgs.earthquake.nshmp.data.MutableXySequence;
 import gov.usgs.earthquake.nshmp.data.XySequence;
-import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.eq.model.SourceType;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
@@ -29,13 +26,11 @@ import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.HazardController;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil.Key;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
 import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
 
 import io.micronaut.http.HttpResponse;
@@ -70,10 +65,10 @@ public final class HazardService {
       var usage = new SourceServices.ResponseData();
       var response = new Response<>(Status.USAGE, NAME, urlHelper.url, usage, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse));
+      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }
 
@@ -97,10 +92,10 @@ public final class HazardService {
       var data = new RequestData(query, Vs30.fromValue(query.vs30));
       var response = process(data, timer, urlHelper);
       var svcResponse = ServletUtil.GSON.toJson(response);
-      LOGGER.info(NAME + " - Response:\n" + ServletUtil.GSON.toJson(svcResponse));
+      LOGGER.info(NAME + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }
 
@@ -108,7 +103,10 @@ public final class HazardService {
       RequestData data,
       Timer timer,
       UrlHelper urlHelper) throws InterruptedException, ExecutionException {
-    var hazard = calc(data);
+    var configFunction = new ConfigFunction();
+    var siteFunction = new SiteFunction(data);
+    var hazard = ServicesUtil.calcHazard(configFunction, siteFunction);
+
     return new ResultBuilder()
         .hazard(hazard)
         .requestData(data)
@@ -117,41 +115,31 @@ public final class HazardService {
         .build();
   }
 
-  static Hazard calc(RequestData data) throws InterruptedException, ExecutionException {
-    var futuresList = ServletUtil.installedModel().models().stream()
-        .map(baseModel -> calcHazard(baseModel, ServletUtil.hazardModels().get(baseModel), data))
-        .collect(Collectors.toList());
-
-    var hazardsFuture = CompletableFuture
-        .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()]))
-        .thenApply(v -> {
-          return futuresList.stream()
-              .map(future -> future.join())
-              .collect(Collectors.toList());
-        });
-
-    var hazards = hazardsFuture.get().toArray(new Hazard[] {});
-    return Hazard.merge(hazards);
+  static class ConfigFunction implements Function<BaseModel, CalcConfig> {
+    @Override
+    public CalcConfig apply(BaseModel baseModel) {
+      var hazardModel = ServletUtil.hazardModels().get(baseModel);
+      var configBuilder = CalcConfig.copyOf(hazardModel.config());
+      configBuilder.imts(baseModel.imts);
+      return configBuilder.build();
+    }
   }
 
-  static CompletableFuture<Hazard> calcHazard(
-      BaseModel baseModel,
-      HazardModel hazardModel,
-      RequestData data) {
-    var location = Location.create(data.latitude, data.longitude);
-    var configBuilder = CalcConfig.copyOf(hazardModel.config());
-    configBuilder.imts(baseModel.imts);
-    var config = configBuilder.build();
-    var site = Site.builder()
-        .basinDataProvider(config.siteData.basinDataProvider)
-        .location(location)
-        .vs30(data.vs30.value())
-        .build();
+  static class SiteFunction implements Function<CalcConfig, Site> {
+    final RequestData data;
 
-    return CompletableFuture
-        .supplyAsync(
-            () -> HazardCalcs.hazard(hazardModel, config, site, ServletUtil.CALC_EXECUTOR),
-            ServletUtil.TASK_EXECUTOR);
+    private SiteFunction(RequestData data) {
+      this.data = data;
+    }
+
+    @Override
+    public Site apply(CalcConfig config) {
+      return Site.builder()
+          .basinDataProvider(config.siteData.basinDataProvider)
+          .location(Location.create(data.latitude, data.longitude))
+          .vs30(data.vs30.value())
+          .build();
+    }
   }
 
   public static class Query extends ServiceQueryData {
@@ -174,7 +162,7 @@ public final class HazardService {
     }
   }
 
-  static final class RequestData extends ServiceRequestData {
+  static class RequestData extends ServiceRequestData {
     final Vs30 vs30;
 
     RequestData(Query query, Vs30 vs30) {
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
index b24499194e513af09f819943eddda6ffa80c6d29..d88510f5baf37aa7d3ddd9c55f065365e680ee6c 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/RateService.java
@@ -21,15 +21,13 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.mfd.Mfds;
 import gov.usgs.earthquake.nshmp.www.RateController;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.ServletUtil.Timer;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil.Key;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceQueryData;
-import gov.usgs.earthquake.nshmp.www.WsUtil.ServiceRequestData;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata.DefaultParameters;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.Key;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceQueryData;
+import gov.usgs.earthquake.nshmp.www.services.ServicesUtil.ServiceRequestData;
+import gov.usgs.earthquake.nshmp.www.services.ServletUtil.Timer;
 
 import io.micronaut.http.HttpResponse;
 
@@ -66,7 +64,7 @@ public final class RateService {
       var svcResponse = ServletUtil.GSON.toJson(response);
       return HttpResponse.ok(String.format(svcResponse, urlHelper.urlPrefix, urlHelper.urlPrefix));
     } catch (Exception e) {
-      return WsUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
     }
   }
 
@@ -98,7 +96,7 @@ public final class RateService {
       LOGGER.info(service.name + " - Response:\n" + svcResponse);
       return HttpResponse.ok(svcResponse);
     } catch (Exception e) {
-      return WsUtil.handleError(e, service.name, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, service.name, LOGGER, urlHelper);
     }
   }
 
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3ef2cb6e46c2893e78789beecd021866731255d
--- /dev/null
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServicesUtil.java
@@ -0,0 +1,139 @@
+package gov.usgs.earthquake.nshmp.www.services;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import com.google.gson.GsonBuilder;
+
+import gov.usgs.earthquake.nshmp.calc.CalcConfig;
+import gov.usgs.earthquake.nshmp.calc.Hazard;
+import gov.usgs.earthquake.nshmp.calc.HazardCalcs;
+import gov.usgs.earthquake.nshmp.calc.Site;
+import gov.usgs.earthquake.nshmp.internal.www.NshmpMicronautServlet.UrlHelper;
+import gov.usgs.earthquake.nshmp.internal.www.Response;
+import gov.usgs.earthquake.nshmp.internal.www.WsUtils;
+import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
+import gov.usgs.earthquake.nshmp.www.BaseModel;
+import gov.usgs.earthquake.nshmp.www.services.SourceServices.SourceModel;
+
+import io.micronaut.http.HttpResponse;
+
+class ServicesUtil {
+
+  static HttpResponse<String> handleError(
+      Throwable e,
+      String name,
+      Logger logger,
+      UrlHelper urlHelper) {
+    var msg = e.getMessage() + " (see logs)";
+    var svcResponse = new Response<>(Status.ERROR, name, urlHelper.url, msg, urlHelper);
+    var gson = new GsonBuilder().setPrettyPrinting().create();
+    var response = gson.toJson(svcResponse);
+    logger.severe(name + " -\n" + response);
+    e.printStackTrace();
+    return HttpResponse.serverError(response);
+  }
+
+  static Hazard calcHazard(
+      Function<BaseModel, CalcConfig> configFunction,
+      Function<CalcConfig, Site> siteFunction) throws InterruptedException, ExecutionException {
+    var futuresList = ServletUtil.installedModel().models().stream()
+        .map(baseModel -> {
+          var config = configFunction.apply(baseModel);
+          var site = siteFunction.apply(config);
+          return calcHazard(baseModel, config, site);
+        })
+        .collect(Collectors.toList());
+
+    var hazardsFuture = CompletableFuture
+        .allOf(futuresList.toArray(new CompletableFuture[futuresList.size()]))
+        .thenApply(v -> {
+          return futuresList.stream()
+              .map(future -> future.join())
+              .collect(Collectors.toList());
+        });
+
+    var hazards = hazardsFuture.get().toArray(new Hazard[] {});
+    return Hazard.merge(hazards);
+  }
+
+  static class ServiceQueryData implements ServiceQuery {
+    public final Double longitude;
+    public final Double latitude;
+
+    ServiceQueryData(Double longitude, Double latitude) {
+      this.longitude = longitude;
+      this.latitude = latitude;
+    }
+
+    @Override
+    public boolean isNull() {
+      return longitude == null && latitude == null;
+    }
+
+    @Override
+    public void checkValues() {
+      WsUtils.checkValue(Key.LONGITUDE, longitude);
+      WsUtils.checkValue(Key.LATITUDE, latitude);
+    }
+  }
+
+  static class ServiceRequestData {
+    public final SourceModel model;
+    public final double longitude;
+    public final double latitude;
+
+    public ServiceRequestData(ServiceQueryData query) {
+      model = new SourceModel(ServletUtil.installedModel());
+      longitude = query.longitude;
+      latitude = query.latitude;
+    }
+  }
+
+  enum Key {
+    EDITION,
+    REGION,
+    MODEL,
+    VS30,
+    LATITUDE,
+    LONGITUDE,
+    IMT,
+    RETURNPERIOD,
+    DISTANCE,
+    FORMAT,
+    TIMESPAN,
+    BASIN;
+
+    private String label;
+
+    private Key() {
+      label = name().toLowerCase();
+    }
+
+    @Override
+    public String toString() {
+      return label;
+    }
+  }
+
+  private static interface ServiceQuery {
+    boolean isNull();
+
+    void checkValues();
+  }
+
+  private static CompletableFuture<Hazard> calcHazard(
+      BaseModel baseModel,
+      CalcConfig config,
+      Site site) {
+    return CompletableFuture
+        .supplyAsync(
+            () -> HazardCalcs.hazard(
+                ServletUtil.hazardModels().get(baseModel), config, site, ServletUtil.CALC_EXECUTOR),
+            ServletUtil.TASK_EXECUTOR);
+  }
+
+}
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
similarity index 78%
rename from src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java
rename to src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
index 83a58c0724a21e0a796254530229c1b1430d53d9..e11d2e22e19cf7f4c0e74453c0d8be5c1d287fdf 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/ServletUtil.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/ServletUtil.java
@@ -1,6 +1,5 @@
-package gov.usgs.earthquake.nshmp.www;
+package gov.usgs.earthquake.nshmp.www.services;
 
-import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.lang.Runtime.getRuntime;
 
 import java.net.URI;
@@ -14,13 +13,9 @@ import java.time.format.DateTimeFormatter;
 import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-import javax.servlet.ServletContext;
-import javax.servlet.http.HttpServletRequest;
-
 import com.google.common.base.Stopwatch;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
@@ -33,6 +28,8 @@ import gov.usgs.earthquake.nshmp.calc.Vs30;
 import gov.usgs.earthquake.nshmp.eq.model.HazardModel;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
+import gov.usgs.earthquake.nshmp.www.BaseModel;
+import gov.usgs.earthquake.nshmp.www.Model;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Region;
 
@@ -48,25 +45,25 @@ import io.micronaut.runtime.event.annotation.EventListener;
  */
 public class ServletUtil {
 
-  @Value("${nshmp-haz.installed-model}")
-  private Model model;
-
-  private static Model INSTALLED_MODEL;
-  private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class);
+  public static final Gson GSON;
   public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern(
       "yyyy-MM-dd'T'HH:mm:ssXXX");
 
-  public static final ListeningExecutorService CALC_EXECUTOR;
-  public static final ExecutorService TASK_EXECUTOR;
+  static final ListeningExecutorService CALC_EXECUTOR;
+  static final ExecutorService TASK_EXECUTOR;
 
-  public static final int THREAD_COUNT;
-
-  public static final Gson GSON;
+  static final int THREAD_COUNT;
 
   /* Stateful flag to reject requests while a result is pending. */
-  public static boolean uhtBusy = false;
-  public static long hitCount = 0;
-  public static long missCount = 0;
+  static boolean uhtBusy = false;
+  static long hitCount = 0;
+  static long missCount = 0;
+
+  @Value("${nshmp-haz.installed-model}")
+  private Model model;
+
+  private static Model INSTALLED_MODEL;
+  private static Map<BaseModel, HazardModel> HAZARD_MODELS = new EnumMap<>(BaseModel.class);
 
   static {
     /* TODO modified for deagg-epsilon branch; should be context var */
@@ -87,11 +84,11 @@ public class ServletUtil {
         .create();
   }
 
-  public static Model installedModel() {
+  static Model installedModel() {
     return INSTALLED_MODEL;
   }
 
-  public static Map<BaseModel, HazardModel> hazardModels() {
+  static Map<BaseModel, HazardModel> hazardModels() {
     return HAZARD_MODELS;
   }
 
@@ -157,11 +154,6 @@ public class ServletUtil {
     }
   }
 
-  static boolean emptyRequest(HttpServletRequest request) {
-    return isNullOrEmpty(request.getQueryString()) &&
-        (request.getPathInfo() == null || request.getPathInfo().equals("/"));
-  }
-
   public static Timer timer() {
     return new Timer();
   }
@@ -171,11 +163,10 @@ public class ServletUtil {
    * be started later.
    */
   public static final class Timer {
-
     Stopwatch servlet = Stopwatch.createStarted();
     Stopwatch calc = Stopwatch.createUnstarted();
 
-    Timer start() {
+    public Timer start() {
       calc.start();
       return this;
     }
@@ -189,32 +180,4 @@ public class ServletUtil {
     }
   }
 
-  public abstract static class TimedTask<T> implements Callable<T> {
-
-    final String url;
-    final Timer timer;
-
-    public TimedTask(String url) {
-      this.url = url;
-      this.timer = ServletUtil.timer();
-    }
-
-    public abstract T calc() throws Exception;
-
-    @Override
-    public T call() throws Exception {
-      timer.start();
-      return calc();
-    }
-  }
-
-  public abstract static class TimedTaskContext<T> extends TimedTask<T> {
-    ServletContext context;
-
-    public TimedTaskContext(String url, ServletContext context) {
-      super(url);
-      this.context = context;
-    }
-  }
-
 }
diff --git a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
index 8201509398c34895250165517a3c19b26f14aa3f..99c80fb53d9eb1befd88f6ed6f7bd379ededb303 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/www/services/SourceServices.java
@@ -23,8 +23,6 @@ import gov.usgs.earthquake.nshmp.internal.www.meta.ParamType;
 import gov.usgs.earthquake.nshmp.internal.www.meta.Status;
 import gov.usgs.earthquake.nshmp.www.BaseModel;
 import gov.usgs.earthquake.nshmp.www.Model;
-import gov.usgs.earthquake.nshmp.www.ServletUtil;
-import gov.usgs.earthquake.nshmp.www.WsUtil;
 import gov.usgs.earthquake.nshmp.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.meta.MetaUtil;
 import gov.usgs.earthquake.nshmp.www.meta.Metadata;
@@ -71,7 +69,7 @@ public class SourceServices {
       LOGGER.info(NAME + "- Response:\n" + jsonString);
       return HttpResponse.ok(jsonString);
     } catch (Exception e) {
-      return WsUtil.handleError(e, NAME, LOGGER, urlHelper);
+      return ServicesUtil.handleError(e, NAME, LOGGER, urlHelper);
     }
   }