Move routing from incubator to a separate app

Change-Id: I961d10af99c572b1f8d9b3d37c6f52dd04422007
diff --git a/apps/route-service/app/BUCK b/apps/route-service/app/BUCK
new file mode 100644
index 0000000..289fddc
--- /dev/null
+++ b/apps/route-service/app/BUCK
@@ -0,0 +1,19 @@
+COMPILE_DEPS = [
+    '//lib:CORE_DEPS',
+    '//lib:concurrent-trees',
+    '//core/store/serializers:onos-core-serializers',
+    '//apps/route-service/api:onos-apps-route-service-api',
+    '//cli:onos-cli',
+    '//lib:org.apache.karaf.shell.console',
+]
+
+TEST_DEPS = [
+    '//lib:TEST',
+    '//apps/route-service/api:onos-apps-route-service-api-tests',
+    '//core/api:onos-api-tests',
+]
+
+osgi_jar_with_tests (
+    deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
+)
diff --git a/apps/route-service/app/app.xml b/apps/route-service/app/app.xml
new file mode 100644
index 0000000..a561826
--- /dev/null
+++ b/apps/route-service/app/app.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015-present Open Networking Foundation
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.routeservice" origin="ON.Lab" version="${project.version}"
+     category="Utility" url="http://onosproject.org" title="Route Service App"
+     featuresRepo="mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features"
+     features="${project.artifactId}">
+    <description>${project.description}</description>
+    <artifact>mvn:${project.groupId}/${project.artifactId}/${project.version}</artifact>
+    <artifact>mvn:${project.groupId}/onos-app-route-service-api/${project.version}</artifact>
+</app>
diff --git a/apps/route-service/app/features.xml b/apps/route-service/app/features.xml
new file mode 100644
index 0000000..4cc99bd
--- /dev/null
+++ b/apps/route-service/app/features.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Copyright 2015-present Open Networking Foundation
+  ~
+  ~ 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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${project.version}">
+    <feature name="${project.artifactId}" version="${project.version}"
+             description="${project.description}">
+        <feature>onos-api</feature>
+        <bundle>mvn:${project.groupId}/onos-app-route-service-api/${project.version}</bundle>
+        <bundle>mvn:${project.groupId}/onos-app-route-service/${project.version}</bundle>
+    </feature>
+</features>
diff --git a/apps/route-service/app/pom.xml b/apps/route-service/app/pom.xml
new file mode 100644
index 0000000..9428fb2
--- /dev/null
+++ b/apps/route-service/app/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015-present Open Networking Foundation
+  ~
+  ~ 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.
+  -->
+<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/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>onos-app-route-service</artifactId>
+        <groupId>org.onosproject</groupId>
+        <version>1.11.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>onos-app-route-service-app</artifactId>
+    <packaging>bundle</packaging>
+
+    <url>http://onosproject.org</url>
+
+    <description>Route Service Application</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-incubator-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-app-route-service-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.googlecode.concurrent-trees</groupId>
+            <artifactId>concurrent-trees</artifactId>
+            <version>2.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>5.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+
+</project>
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteAddCommand.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteAddCommand.java
new file mode 100644
index 0000000..1810a07
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteAddCommand.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+
+import java.util.Collections;
+
+/**
+ * Command to add a route to the routing table.
+ */
+@Command(scope = "onos", name = "route-add",
+        description = "Adds a route to the route table")
+public class RouteAddCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "prefix", description = "IP prefix of the route",
+            required = true)
+    String prefixString = null;
+
+    @Argument(index = 1, name = "nextHop", description = "IP address of the next hop",
+            required = true)
+    String nextHopString = null;
+
+    @Override
+    protected void execute() {
+        RouteAdminService service = AbstractShellCommand.get(RouteAdminService.class);
+
+        IpPrefix prefix = IpPrefix.valueOf(prefixString);
+        IpAddress nextHop = IpAddress.valueOf(nextHopString);
+
+        service.update(Collections.singleton(new Route(Route.Source.STATIC, prefix, nextHop)));
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteRemoveCommand.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteRemoveCommand.java
new file mode 100644
index 0000000..9fef2ec
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteRemoveCommand.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.cli;
+
+import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.commands.Command;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+
+import java.util.Collections;
+
+/**
+ * Command to remove a route from the routing table.
+ */
+@Command(scope = "onos", name = "route-remove",
+        description = "Removes a route from the route table")
+public class RouteRemoveCommand extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "prefix", description = "IP prefix of the route",
+            required = true)
+    String prefixString = null;
+
+    @Argument(index = 1, name = "nextHop", description = "IP address of the next hop",
+            required = true)
+    String nextHopString = null;
+
+    @Override
+    protected void execute() {
+        RouteAdminService service = AbstractShellCommand.get(RouteAdminService.class);
+
+        IpPrefix prefix = IpPrefix.valueOf(prefixString);
+        IpAddress nextHop = IpAddress.valueOf(nextHopString);
+
+        service.withdraw(Collections.singleton(new Route(Route.Source.STATIC, prefix, nextHop)));
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteStoreCommand.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteStoreCommand.java
new file mode 100644
index 0000000..2aefbf7
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RouteStoreCommand.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.routeservice.cli;
+
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.routeservice.RouteStore;
+
+/**
+ * Command to show the current route store implementation.
+ */
+@Command(scope = "onos", name = "route-store",
+        description = "Show the current route store implementation.")
+public class RouteStoreCommand extends AbstractShellCommand {
+
+    @Override
+    protected void execute() {
+        RouteStore routeStore = AbstractShellCommand.get(RouteStore.class);
+        print(routeStore.name());
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RoutesListCommand.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RoutesListCommand.java
new file mode 100644
index 0000000..9c7eae0
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/RoutesListCommand.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+package org.onosproject.routeservice.cli;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.karaf.shell.commands.Command;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteInfo;
+import org.onosproject.routeservice.RouteService;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Command to show the routes in the routing tables.
+ */
+@Command(scope = "onos", name = "routes",
+        description = "Lists routes in the route store")
+public class RoutesListCommand extends AbstractShellCommand {
+
+    private static final String NETWORK = "Network";
+    private static final String NEXTHOP = "Next Hop";
+    private static final String SOURCE = "Source";
+    private static final String NODE = "Node";
+
+    private static final String FORMAT_ROUTE = "%-1s   %-18s %-15s %s (%s)";
+    private static final String FORMAT_ROUTE6 = "%-1s   %-43s %-39s %s (%s)";
+
+    private static final String FORMAT_TABLE = "Table: %s";
+    private static final String FORMAT_TOTAL = "   Total: %d";
+
+    @Override
+    protected void execute() {
+        RouteService service = AbstractShellCommand.get(RouteService.class);
+
+        if (outputJson()) {
+            ObjectMapper mapper = new ObjectMapper();
+            ObjectNode result = mapper.createObjectNode();
+            result.set("routes4", json(service.getRoutes(new RouteTableId("ipv4"))));
+            result.set("routes6", json(service.getRoutes(new RouteTableId("ipv6"))));
+            print("%s", result);
+        } else {
+            service.getRouteTables().forEach(id -> {
+                Collection<RouteInfo> tableRoutes = service.getRoutes(id);
+
+                String format = tableRoutes.stream().anyMatch(route -> route.prefix().isIp6()) ?
+                        FORMAT_ROUTE6 : FORMAT_ROUTE;
+
+                // Print header
+                print(FORMAT_TABLE, id);
+                print(format, "", NETWORK, NEXTHOP, SOURCE, NODE);
+
+                // Print routing entries
+                tableRoutes.stream()
+                        .sorted(Comparator.comparing(r -> r.prefix().address()))
+                        .forEach(route -> this.print(format, route));
+
+                print(FORMAT_TOTAL, tableRoutes.size());
+                print("");
+            });
+        }
+    }
+
+    private void print(String format, RouteInfo routeInfo) {
+        routeInfo.allRoutes().stream()
+                .sorted(Comparator.comparing(r -> r.nextHop()))
+                .forEach(r -> print(format, isBestRoute(routeInfo.bestRoute(), r) ? ">" : "",
+                        r.prefix(), r.nextHop(), r.route().source(), r.route().sourceNode()));
+    }
+
+    private boolean isBestRoute(Optional<ResolvedRoute> bestRoute, ResolvedRoute route) {
+        return Objects.equals(bestRoute.orElse(null), route);
+    }
+
+    /**
+     * Produces a JSON array of routes.
+     *
+     * @param routes the routes with the data
+     * @return JSON array with the routes
+     */
+    private JsonNode json(Collection<RouteInfo> routes) {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode result = mapper.createArrayNode();
+
+        routes.stream()
+                .flatMap(ri -> ri.allRoutes().stream())
+                .forEach(r -> result.add(json(mapper, r)));
+
+        return result;
+    }
+
+    /**
+     * Produces JSON object for a route.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param route the route with the data
+     * @return JSON object for the route
+     */
+    private ObjectNode json(ObjectMapper mapper, ResolvedRoute route) {
+        ObjectNode result = mapper.createObjectNode();
+
+        result.put("prefix", route.prefix().toString());
+        result.put("nextHop", route.nextHop().toString());
+
+        return result;
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/package-info.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/package-info.java
new file mode 100644
index 0000000..3880e5b
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Implementation of route service.
+ */
+package org.onosproject.routeservice.cli;
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ConfigurationRouteSource.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ConfigurationRouteSource.java
new file mode 100644
index 0000000..eb6733f
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ConfigurationRouteSource.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+import org.onosproject.routeservice.RouteConfig;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.config.basics.SubjectFactories;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Route source that installs static routes configured in the network configuration.
+ */
+@Component(immediate = true)
+public class ConfigurationRouteSource {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected NetworkConfigRegistry netcfgRegistry;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RouteAdminService routeService;
+
+    private final ConfigFactory<ApplicationId, RouteConfig> routeConfigFactory =
+            new ConfigFactory<ApplicationId, RouteConfig>(
+                    SubjectFactories.APP_SUBJECT_FACTORY,
+                    RouteConfig.class, "routes", true) {
+                @Override
+                public RouteConfig createConfig() {
+                    return new RouteConfig();
+                }
+            };
+    private final InternalNetworkConfigListener netcfgListener =
+            new InternalNetworkConfigListener();
+
+    @Activate
+    protected void activate() {
+        netcfgRegistry.addListener(netcfgListener);
+        netcfgRegistry.registerConfigFactory(routeConfigFactory);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        netcfgRegistry.removeListener(netcfgListener);
+        netcfgRegistry.unregisterConfigFactory(routeConfigFactory);
+    }
+
+    private void processRouteConfigAdded(NetworkConfigEvent event) {
+        Set<Route> routes = ((RouteConfig) event.config().get()).getRoutes();
+        routeService.update(routes);
+    }
+
+    private void processRouteConfigUpdated(NetworkConfigEvent event) {
+        Set<Route> routes = ((RouteConfig) event.config().get()).getRoutes();
+        Set<Route> prevRoutes = ((RouteConfig) event.prevConfig().get()).getRoutes();
+        Set<Route> pendingRemove = prevRoutes.stream()
+                .filter(prevRoute -> routes.stream()
+                        .noneMatch(route -> route.prefix().equals(prevRoute.prefix())))
+                .collect(Collectors.toSet());
+        Set<Route> pendingUpdate = routes.stream()
+                .filter(route -> !pendingRemove.contains(route)).collect(Collectors.toSet());
+        routeService.update(pendingUpdate);
+        routeService.withdraw(pendingRemove);
+    }
+
+    private void processRouteConfigRemoved(NetworkConfigEvent event) {
+        Set<Route> prevRoutes = ((RouteConfig) event.prevConfig().get()).getRoutes();
+        routeService.withdraw(prevRoutes);
+    }
+
+    private class InternalNetworkConfigListener implements
+            NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            if (event.configClass().equals(RouteConfig.class)) {
+                switch (event.type()) {
+                case CONFIG_ADDED:
+                    processRouteConfigAdded(event);
+                    break;
+                case CONFIG_UPDATED:
+                    processRouteConfigUpdated(event);
+                    break;
+                case CONFIG_REMOVED:
+                    processRouteConfigRemoved(event);
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/DefaultResolvedRouteStore.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/DefaultResolvedRouteStore.java
new file mode 100644
index 0000000..34aeaa7
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/DefaultResolvedRouteStore.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.googlecode.concurrenttrees.common.KeyValuePair;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.GuavaCollectors;
+import org.onlab.util.Tools;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.onosproject.routeservice.RouteTools.createBinaryString;
+
+/**
+ * Stores routes that have been resolved.
+ */
+public class DefaultResolvedRouteStore implements ResolvedRouteStore {
+
+    private Map<RouteTableId, RouteTable> routeTables;
+    private static final RouteTableId IPV4 = new RouteTableId("ipv4");
+    private static final RouteTableId IPV6 = new RouteTableId("ipv6");
+
+    /**
+     * Creates a new resolved route store.
+     */
+    public DefaultResolvedRouteStore() {
+        routeTables = new ConcurrentHashMap<>();
+
+        routeTables.put(IPV4, new RouteTable());
+        routeTables.put(IPV6, new RouteTable());
+    }
+
+    @Override
+    public RouteEvent updateRoute(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+        return getDefaultRouteTable(route).update(route, alternatives);
+    }
+
+    @Override
+    public RouteEvent removeRoute(IpPrefix prefix) {
+        RouteTable table = getDefaultRouteTable(prefix.address());
+        return table.remove(prefix);
+    }
+
+    @Override
+    public Set<RouteTableId> getRouteTables() {
+        return routeTables.keySet();
+    }
+
+    @Override
+    public Collection<ResolvedRoute> getRoutes(RouteTableId table) {
+        RouteTable routeTable = routeTables.get(table);
+        if (routeTable == null) {
+            return Collections.emptySet();
+        }
+        return routeTable.getRoutes();
+    }
+
+    @Override
+    public Optional<ResolvedRoute> getRoute(IpPrefix prefix) {
+        return getDefaultRouteTable(prefix.address()).getRoute(prefix);
+    }
+
+    @Override
+    public Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix) {
+        return getDefaultRouteTable(prefix.address()).getAllRoutes(prefix);
+    }
+
+    @Override
+    public Optional<ResolvedRoute> longestPrefixMatch(IpAddress ip) {
+        return getDefaultRouteTable(ip).longestPrefixMatch(ip);
+    }
+
+    private RouteTable getDefaultRouteTable(ResolvedRoute route) {
+        return getDefaultRouteTable(route.prefix().address());
+    }
+
+    private RouteTable getDefaultRouteTable(IpAddress ip) {
+        RouteTableId routeTableId = (ip.isIp4()) ? IPV4 : IPV6;
+        return routeTables.get(routeTableId);
+    }
+
+    /**
+     * Route table into which routes can be placed.
+     */
+    private class RouteTable {
+        private final InvertedRadixTree<ResolvedRoute> routeTable;
+        private final Map<IpPrefix, Set<ResolvedRoute>> alternativeRoutes;
+
+        /**
+         * Creates a new route table.
+         */
+        public RouteTable() {
+            routeTable = new ConcurrentInvertedRadixTree<>(
+                    new DefaultByteArrayNodeFactory());
+
+            alternativeRoutes = Maps.newHashMap();
+        }
+
+        /**
+         * Adds or updates the route in the route table.
+         *
+         * @param route route to update
+         * @param alternatives alternative routes
+         */
+        public RouteEvent update(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+            Set<ResolvedRoute> immutableAlternatives = checkAlternatives(route, alternatives);
+
+            synchronized (this) {
+                ResolvedRoute oldRoute = routeTable.put(createBinaryString(route.prefix()), route);
+                Set<ResolvedRoute> oldRoutes = alternativeRoutes.put(route.prefix(), immutableAlternatives);
+
+                if (!route.equals(oldRoute)) {
+                    if (oldRoute == null) {
+                        return new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route,
+                                immutableAlternatives);
+                    } else {
+                        return new RouteEvent(RouteEvent.Type.ROUTE_UPDATED, route,
+                                oldRoute, immutableAlternatives);
+                    }
+                }
+
+                if (!immutableAlternatives.equals(oldRoutes)) {
+                    return new RouteEvent(RouteEvent.Type.ALTERNATIVE_ROUTES_CHANGED,
+                            route, immutableAlternatives);
+                }
+
+                return null;
+            }
+        }
+
+        /**
+         * Checks that the best route is present in the alternatives list and
+         * returns an immutable set of alternatives.
+         *
+         * @param route best route
+         * @param alternatives alternatives
+         * @return immutable set of alternative routes
+         */
+        private Set<ResolvedRoute> checkAlternatives(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+            if (!alternatives.contains(route)) {
+                return ImmutableSet.<ResolvedRoute>builder()
+                        .addAll(alternatives)
+                        .add(route)
+                        .build();
+            } else {
+                return ImmutableSet.copyOf(alternatives);
+            }
+        }
+
+        /**
+         * Removes the route from the route table.
+         *
+         * @param prefix prefix to remove
+         */
+        public RouteEvent remove(IpPrefix prefix) {
+            synchronized (this) {
+                String key = createBinaryString(prefix);
+
+                ResolvedRoute route = routeTable.getValueForExactKey(key);
+                alternativeRoutes.remove(prefix);
+
+                if (route != null) {
+                    routeTable.remove(key);
+                    return new RouteEvent(RouteEvent.Type.ROUTE_REMOVED, route);
+                }
+                return null;
+            }
+        }
+
+        /**
+         * Returns all routes in the route table.
+         *
+         * @return all routes
+         */
+        public Collection<ResolvedRoute> getRoutes() {
+            return Tools.stream(routeTable.getKeyValuePairsForKeysStartingWith(""))
+                    .map(KeyValuePair::getValue)
+                    .collect(GuavaCollectors.toImmutableList());
+        }
+
+        /**
+         * Returns the best route for the given prefix, if one exists.
+         *
+         * @param prefix IP prefix
+         * @return best route
+         */
+        public Optional<ResolvedRoute> getRoute(IpPrefix prefix) {
+            return Optional.ofNullable(routeTable.getValueForExactKey(createBinaryString(prefix)));
+        }
+
+        public Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix) {
+            return alternativeRoutes.getOrDefault(prefix, Collections.emptySet());
+        }
+
+        /**
+         * Performs a longest prefix match with the given IP in the route table.
+         *
+         * @param ip IP address to look up
+         * @return most specific prefix containing the given
+         */
+        public Optional<ResolvedRoute> longestPrefixMatch(IpAddress ip) {
+            return Tools.stream(routeTable.getValuesForKeysPrefixing(createBinaryString(ip.toIpPrefix())))
+                    .reduce((a, b) -> b); // reduces to the last element in the stream
+        }
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ListenerQueue.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ListenerQueue.java
new file mode 100644
index 0000000..93e80d5
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ListenerQueue.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import org.onosproject.routeservice.RouteEvent;
+
+/**
+ * Queues updates for a route listener to ensure they are received in the
+ * correct order.
+ */
+interface ListenerQueue {
+
+    /**
+     * Posts an event to the listener.
+     *
+     * @param event event
+     */
+    void post(RouteEvent event);
+
+    /**
+     * Initiates event delivery to the listener.
+     */
+    void start();
+
+    /**
+     * Halts event delivery to the listener.
+     */
+    void stop();
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ResolvedRouteStore.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ResolvedRouteStore.java
new file mode 100644
index 0000000..a6db108
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/ResolvedRouteStore.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Stores resolved routes and best route decisions.
+ */
+public interface ResolvedRouteStore {
+
+    /**
+     * Adds or updates the best route for the given prefix.
+     *
+     * @param route new best route for this prefix
+     * @param alternatives alternative resolved routes
+     * @return event describing the change
+     */
+    RouteEvent updateRoute(ResolvedRoute route, Set<ResolvedRoute> alternatives);
+
+    /**
+     * Removes the best route for the given prefix.
+     *
+     * @param prefix IP prefix
+     * @return event describing the change
+     */
+    RouteEvent removeRoute(IpPrefix prefix);
+
+    /**
+     * Gets the set of route tables.
+     *
+     * @return set of route table IDs
+     */
+    Set<RouteTableId> getRouteTables();
+
+    /**
+     * Returns the best routes for a give route table.
+     *
+     * @param table route table ID
+     * @return collection of selected routes
+     */
+    Collection<ResolvedRoute> getRoutes(RouteTableId table);
+
+    /**
+     * Returns the best selected route for the given IP prefix.
+     *
+     * @param prefix IP prefix
+     * @return optional best route
+     */
+    Optional<ResolvedRoute> getRoute(IpPrefix prefix);
+
+    /**
+     * Returns all resolved routes stored for the given prefix, including the
+     * best selected route.
+     *
+     * @param prefix IP prefix to look up routes for
+     * @return all stored resolved routes for this prefix
+     */
+    Collection<ResolvedRoute> getAllRoutes(IpPrefix prefix);
+
+    /**
+     * Performs a longest prefix match of the best routes on the given IP address.
+     *
+     * @param ip IP address
+     * @return optional longest matching route
+     */
+    Optional<ResolvedRoute> longestPrefixMatch(IpAddress ip);
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteManager.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteManager.java
new file mode 100644
index 0000000..e5db364
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteManager.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.routeservice.InternalRouteEvent;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteInfo;
+import org.onosproject.routeservice.RouteListener;
+import org.onosproject.routeservice.RouteService;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteStore;
+import org.onosproject.routeservice.RouteStoreDelegate;
+import org.onosproject.routeservice.RouteTableId;
+import org.onosproject.net.Host;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.concurrent.GuardedBy;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.stream.Collectors;
+
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Implementation of the unicast route service.
+ */
+@Service
+@Component
+public class RouteManager implements RouteService, RouteAdminService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private RouteStoreDelegate delegate = new InternalRouteStoreDelegate();
+    private InternalHostListener hostListener = new InternalHostListener();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected RouteStore routeStore;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected HostService hostService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    private ResolvedRouteStore resolvedRouteStore;
+
+    private RouteMonitor routeMonitor;
+
+    @GuardedBy(value = "this")
+    private Map<RouteListener, ListenerQueue> listeners = new HashMap<>();
+
+    private ThreadFactory threadFactory;
+
+    @Activate
+    protected void activate() {
+        routeMonitor = new RouteMonitor(this, clusterService, storageService);
+        threadFactory = groupedThreads("onos/route", "listener-%d", log);
+
+        resolvedRouteStore = new DefaultResolvedRouteStore();
+
+        routeStore.setDelegate(delegate);
+        hostService.addListener(hostListener);
+
+        routeStore.getRouteTables().stream()
+                .flatMap(id -> routeStore.getRoutes(id).stream())
+                .forEach(this::resolve);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        routeMonitor.shutdown();
+        listeners.values().forEach(ListenerQueue::stop);
+
+        routeStore.unsetDelegate(delegate);
+        hostService.removeListener(hostListener);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * In a departure from other services in ONOS, calling addListener will
+     * cause all current routes to be pushed to the listener before any new
+     * events are sent. This allows a listener to easily get the exact set of
+     * routes without worrying about missing any.
+     *
+     * @param listener listener to be added
+     */
+    @Override
+    public void addListener(RouteListener listener) {
+        synchronized (this) {
+            log.debug("Synchronizing current routes to new listener");
+            ListenerQueue l = createListenerQueue(listener);
+            resolvedRouteStore.getRouteTables().stream()
+                    .map(resolvedRouteStore::getRoutes)
+                    .flatMap(Collection::stream)
+                    .map(route -> new RouteEvent(RouteEvent.Type.ROUTE_ADDED, route,
+                                                 resolvedRouteStore.getAllRoutes(route.prefix())))
+                    .forEach(l::post);
+
+            listeners.put(listener, l);
+
+            l.start();
+            log.debug("Route synchronization complete");
+        }
+    }
+
+    @Override
+    public void removeListener(RouteListener listener) {
+        synchronized (this) {
+            ListenerQueue l = listeners.remove(listener);
+            if (l != null) {
+                l.stop();
+            }
+        }
+    }
+
+    /**
+     * Posts an event to all listeners.
+     *
+     * @param event event
+     */
+    private void post(RouteEvent event) {
+        if (event != null) {
+            log.debug("Sending event {}", event);
+            synchronized (this) {
+                listeners.values().forEach(l -> l.post(event));
+            }
+        }
+    }
+
+    private Collection<Route> reformatRoutes(Collection<RouteSet> routeSets) {
+        return routeSets.stream().flatMap(r -> r.routes().stream()).collect(Collectors.toList());
+    }
+
+    public Collection<RouteTableId> getRouteTables() {
+        return routeStore.getRouteTables();
+    }
+
+    @Override
+    public Collection<RouteInfo> getRoutes(RouteTableId id) {
+        return routeStore.getRoutes(id).stream()
+                .map(routeSet -> new RouteInfo(routeSet.prefix(),
+                        resolvedRouteStore.getRoute(routeSet.prefix()).orElse(null), resolveRouteSet(routeSet)))
+                .collect(Collectors.toList());
+    }
+
+    private Set<ResolvedRoute> resolveRouteSet(RouteSet routeSet) {
+        return routeSet.routes().stream()
+                .map(this::tryResolve)
+                .collect(Collectors.toSet());
+    }
+
+    private ResolvedRoute tryResolve(Route route) {
+        ResolvedRoute resolvedRoute = resolve(route);
+        if (resolvedRoute == null) {
+            resolvedRoute = new ResolvedRoute(route, null, null);
+        }
+        return resolvedRoute;
+    }
+
+    @Override
+    public Optional<ResolvedRoute> longestPrefixLookup(IpAddress ip) {
+        return resolvedRouteStore.longestPrefixMatch(ip);
+    }
+
+    @Override
+    public void update(Collection<Route> routes) {
+        synchronized (this) {
+            routes.forEach(route -> {
+                log.debug("Received update {}", route);
+                routeStore.updateRoute(route);
+            });
+        }
+    }
+
+    @Override
+    public void withdraw(Collection<Route> routes) {
+        synchronized (this) {
+            routes.forEach(route -> {
+                log.debug("Received withdraw {}", route);
+                routeStore.removeRoute(route);
+            });
+        }
+    }
+
+    @Override
+    public Route longestPrefixMatch(IpAddress ip) {
+        return longestPrefixLookup(ip)
+                .map(ResolvedRoute::route)
+                .orElse(null);
+    }
+
+    private ResolvedRoute resolve(Route route) {
+        hostService.startMonitoringIp(route.nextHop());
+        Set<Host> hosts = hostService.getHostsByIp(route.nextHop());
+
+        Optional<Host> host = hosts.stream().findFirst();
+        if (host.isPresent()) {
+            return new ResolvedRoute(route, host.get().mac(), host.get().vlan(),
+                    host.get().location());
+        } else {
+            return null;
+        }
+    }
+
+    private ResolvedRoute decide(ResolvedRoute route1, ResolvedRoute route2) {
+        return Comparator.comparing(ResolvedRoute::nextHop)
+                       .compare(route1, route2) <= 0 ? route1 : route2;
+    }
+
+    private void store(ResolvedRoute route, Set<ResolvedRoute> alternatives) {
+        post(resolvedRouteStore.updateRoute(route, alternatives));
+    }
+
+    private void remove(IpPrefix prefix) {
+        post(resolvedRouteStore.removeRoute(prefix));
+    }
+
+    private void resolve(RouteSet routes) {
+        Set<ResolvedRoute> resolvedRoutes = routes.routes().stream()
+                .map(this::resolve)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Optional<ResolvedRoute> bestRoute = resolvedRoutes.stream()
+                    .reduce(this::decide);
+
+        if (bestRoute.isPresent()) {
+            store(bestRoute.get(), resolvedRoutes);
+        } else {
+            remove(routes.prefix());
+        }
+    }
+
+    private void hostUpdated(Host host) {
+        hostChanged(host);
+    }
+
+    private void hostRemoved(Host host) {
+        hostChanged(host);
+    }
+
+    private void hostChanged(Host host) {
+        synchronized (this) {
+            host.ipAddresses().stream()
+                    .flatMap(ip -> routeStore.getRoutesForNextHop(ip).stream())
+                    .map(route -> routeStore.getRoutes(route.prefix()))
+                    .forEach(this::resolve);
+        }
+    }
+
+    /**
+     * Creates a new listener queue.
+     *
+     * @param listener route listener
+     * @return listener queue
+     */
+    ListenerQueue createListenerQueue(RouteListener listener) {
+        return new DefaultListenerQueue(listener);
+    }
+
+    /**
+     * Default route listener queue.
+     */
+    private class DefaultListenerQueue implements ListenerQueue {
+
+        private final ExecutorService executorService;
+        private final BlockingQueue<RouteEvent> queue;
+        private final RouteListener listener;
+
+        /**
+         * Creates a new listener queue.
+         *
+         * @param listener route listener to queue updates for
+         */
+        public DefaultListenerQueue(RouteListener listener) {
+            this.listener = listener;
+            queue = new LinkedBlockingQueue<>();
+            executorService = newSingleThreadExecutor(threadFactory);
+        }
+
+        @Override
+        public void post(RouteEvent event) {
+            queue.add(event);
+        }
+
+        @Override
+        public void start() {
+            executorService.execute(this::poll);
+        }
+
+        @Override
+        public void stop() {
+            executorService.shutdown();
+        }
+
+        private void poll() {
+            while (true) {
+                try {
+                    listener.event(queue.take());
+                } catch (InterruptedException e) {
+                    log.info("Route listener event thread shutting down: {}", e.getMessage());
+                    break;
+                } catch (Exception e) {
+                    log.warn("Exception during route event handler", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Delegate to receive events from the route store.
+     */
+    private class InternalRouteStoreDelegate implements RouteStoreDelegate {
+        @Override
+        public void notify(InternalRouteEvent event) {
+            switch (event.type()) {
+            case ROUTE_ADDED:
+                resolve(event.subject());
+                break;
+            case ROUTE_REMOVED:
+                resolve(event.subject());
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
+    /**
+     * Internal listener for host events.
+     */
+    private class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            switch (event.type()) {
+            case HOST_ADDED:
+            case HOST_UPDATED:
+                hostUpdated(event.subject());
+                break;
+            case HOST_REMOVED:
+                hostRemoved(event.subject());
+                break;
+            case HOST_MOVED:
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteMonitor.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteMonitor.java
new file mode 100644
index 0000000..24d2aff
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/RouteMonitor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import org.onosproject.cluster.ClusterEvent;
+import org.onosproject.cluster.ClusterEventListener;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteAdminService;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.DistributedPrimitive;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WorkQueue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
+
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Monitors cluster nodes and removes routes if a cluster node becomes unavailable.
+ */
+public class RouteMonitor {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    private static final String TOPIC = "route-reaper";
+    private static final int NUM_PARALLEL_JOBS = 10;
+
+    private RouteAdminService routeService;
+    private final ClusterService clusterService;
+    private StorageService storageService;
+
+    private WorkQueue<NodeId> queue;
+
+    private final InternalClusterListener clusterListener = new InternalClusterListener();
+
+    private final ScheduledExecutorService reaperExecutor =
+            newSingleThreadScheduledExecutor(groupedThreads("route/reaper", "", log));
+
+    /**
+     * Creates a new route monitor.
+     *
+     * @param routeService route service
+     * @param clusterService cluster service
+     * @param storageService storage service
+     */
+    public RouteMonitor(RouteAdminService routeService,
+                        ClusterService clusterService, StorageService storageService) {
+        this.routeService = routeService;
+        this.clusterService = clusterService;
+        this.storageService = storageService;
+
+        clusterService.addListener(clusterListener);
+
+        queue = storageService.getWorkQueue(TOPIC, Serializer.using(KryoNamespaces.API));
+        queue.addStatusChangeListener(this::statusChange);
+
+        startProcessing();
+    }
+
+    /**
+     * Shuts down the route monitor.
+     */
+    public void shutdown() {
+        stopProcessing();
+        clusterService.removeListener(clusterListener);
+    }
+
+    private void statusChange(DistributedPrimitive.Status status) {
+        switch (status) {
+        case ACTIVE:
+            startProcessing();
+            break;
+        case SUSPENDED:
+            stopProcessing();
+            break;
+        case INACTIVE:
+        default:
+            break;
+        }
+    }
+
+    private void startProcessing() {
+        queue.registerTaskProcessor(this::cleanRoutes, NUM_PARALLEL_JOBS, reaperExecutor);
+    }
+
+    private void stopProcessing() {
+        queue.stopProcessing();
+    }
+
+    private void cleanRoutes(NodeId node) {
+        log.info("Cleaning routes from unavailable node {}", node);
+
+        Collection<Route> routes = routeService.getRouteTables().stream()
+                .flatMap(id -> routeService.getRoutes(id).stream())
+                .flatMap(route -> route.allRoutes().stream())
+                .map(ResolvedRoute::route)
+                .filter(r -> r.sourceNode().equals(node))
+                .collect(Collectors.toList());
+
+        log.debug("Withdrawing routes: {}", routes);
+
+        routeService.withdraw(routes);
+    }
+
+    private class InternalClusterListener implements ClusterEventListener {
+
+        @Override
+        public void event(ClusterEvent event) {
+            switch (event.type()) {
+            case INSTANCE_DEACTIVATED:
+                NodeId id = event.subject().id();
+                log.info("Node {} deactivated", id);
+                queue.addOne(id);
+                break;
+            case INSTANCE_ADDED:
+            case INSTANCE_REMOVED:
+            case INSTANCE_ACTIVATED:
+            case INSTANCE_READY:
+            default:
+                break;
+            }
+        }
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/package-info.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/package-info.java
new file mode 100644
index 0000000..037bde4
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Implementation of route service.
+ */
+package org.onosproject.routeservice.impl;
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DefaultRouteTable.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DefaultRouteTable.java
new file mode 100644
index 0000000..d42a05a
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DefaultRouteTable.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.routeservice.InternalRouteEvent;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteStoreDelegate;
+import org.onosproject.routeservice.RouteTableId;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.DistributedPrimitive;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Default implementation of a route table based on a consistent map.
+ */
+public class DefaultRouteTable implements RouteTable {
+
+    private final RouteTableId id;
+    private final ConsistentMap<IpPrefix, Set<Route>> routes;
+    private final RouteStoreDelegate delegate;
+    private final ExecutorService executor;
+    private final RouteTableListener listener = new RouteTableListener();
+
+    private final Consumer<DistributedPrimitive.Status> statusChangeListener;
+
+    /**
+     * Creates a new route table.
+     *
+     * @param id route table ID
+     * @param delegate route store delegate to notify of events
+     * @param storageService storage service
+     * @param executor executor service
+     */
+    public DefaultRouteTable(RouteTableId id, RouteStoreDelegate delegate,
+                             StorageService storageService, ExecutorService executor) {
+        this.delegate = checkNotNull(delegate);
+        this.id = checkNotNull(id);
+        this.routes = buildRouteMap(checkNotNull(storageService));
+        this.executor = checkNotNull(executor);
+
+        statusChangeListener = status -> {
+            if (status.equals(DistributedPrimitive.Status.ACTIVE)) {
+                executor.execute(this::notifyExistingRoutes);
+            }
+        };
+        routes.addStatusChangeListener(statusChangeListener);
+
+        notifyExistingRoutes();
+
+        routes.addListener(listener);
+    }
+
+    private void notifyExistingRoutes() {
+        routes.entrySet().stream()
+                .map(e -> new InternalRouteEvent(InternalRouteEvent.Type.ROUTE_ADDED,
+                        new RouteSet(id, e.getKey(), e.getValue().value())))
+                .forEach(delegate::notify);
+    }
+
+    private ConsistentMap<IpPrefix, Set<Route>> buildRouteMap(StorageService storageService) {
+        KryoNamespace routeTableSerializer = KryoNamespace.newBuilder()
+                .register(KryoNamespaces.API)
+                .register(Route.class)
+                .register(Route.Source.class)
+                .build();
+        return storageService.<IpPrefix, Set<Route>>consistentMapBuilder()
+                .withName("onos-routes-" + id.name())
+                .withRelaxedReadConsistency()
+                .withSerializer(Serializer.using(routeTableSerializer))
+                .build();
+    }
+
+    @Override
+    public RouteTableId id() {
+        return id;
+    }
+
+    @Override
+    public void shutdown() {
+        routes.removeStatusChangeListener(statusChangeListener);
+        routes.removeListener(listener);
+    }
+
+    @Override
+    public void destroy() {
+        shutdown();
+        routes.destroy();
+    }
+
+    @Override
+    public void update(Route route) {
+        routes.compute(route.prefix(), (prefix, set) -> {
+            if (set == null) {
+                set = new HashSet<>();
+            }
+            set.add(route);
+            return set;
+        });
+    }
+
+    @Override
+    public void remove(Route route) {
+        routes.compute(route.prefix(), (prefix, set) -> {
+            if (set != null) {
+                set.remove(route);
+                if (set.isEmpty()) {
+                    return null;
+                }
+                return set;
+            }
+            return null;
+        });
+    }
+
+    @Override
+    public Collection<RouteSet> getRoutes() {
+        return routes.entrySet().stream()
+                .map(e -> new RouteSet(id, e.getKey(), e.getValue().value()))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public RouteSet getRoutes(IpPrefix prefix) {
+        Versioned<Set<Route>> routeSet = routes.get(prefix);
+
+        if (routeSet != null) {
+            return new RouteSet(id, prefix, routeSet.value());
+        }
+        return null;
+    }
+
+    @Override
+    public Collection<Route> getRoutesForNextHop(IpAddress nextHop) {
+        // TODO index
+        return routes.values().stream()
+                .flatMap(v -> v.value().stream())
+                .filter(r -> r.nextHop().equals(nextHop))
+                .collect(Collectors.toSet());
+    }
+
+    private class RouteTableListener
+            implements MapEventListener<IpPrefix, Set<Route>> {
+
+        private InternalRouteEvent createRouteEvent(
+                InternalRouteEvent.Type type, MapEvent<IpPrefix, Set<Route>> event) {
+            Set<Route> currentRoutes =
+                    (event.newValue() == null) ? Collections.emptySet() : event.newValue().value();
+            return new InternalRouteEvent(type, new RouteSet(id, event.key(), currentRoutes));
+        }
+
+        @Override
+        public void event(MapEvent<IpPrefix, Set<Route>> event) {
+            InternalRouteEvent ire = null;
+            switch (event.type()) {
+            case INSERT:
+                ire = createRouteEvent(InternalRouteEvent.Type.ROUTE_ADDED, event);
+                break;
+            case UPDATE:
+                if (event.newValue().value().size() > event.oldValue().value().size()) {
+                    ire = createRouteEvent(InternalRouteEvent.Type.ROUTE_ADDED, event);
+                } else {
+                    ire = createRouteEvent(InternalRouteEvent.Type.ROUTE_REMOVED, event);
+                }
+                break;
+            case REMOVE:
+                ire = createRouteEvent(InternalRouteEvent.Type.ROUTE_REMOVED, event);
+                break;
+            default:
+                break;
+            }
+            if (ire != null) {
+                delegate.notify(ire);
+            }
+        }
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DistributedRouteStore.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DistributedRouteStore.java
new file mode 100644
index 0000000..feacb82
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/DistributedRouteStore.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.routeservice.InternalRouteEvent;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteStore;
+import org.onosproject.routeservice.RouteStoreDelegate;
+import org.onosproject.routeservice.RouteTableId;
+import org.onosproject.store.AbstractStore;
+import org.onosproject.store.service.DistributedSet;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.SetEvent;
+import org.onosproject.store.service.SetEventListener;
+import org.onosproject.store.service.StorageService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.onlab.util.Tools.groupedThreads;
+
+/**
+ * Route store based on distributed storage.
+ */
+public class DistributedRouteStore extends AbstractStore<InternalRouteEvent, RouteStoreDelegate>
+        implements RouteStore {
+
+    protected StorageService storageService;
+
+    private static final RouteTableId IPV4 = new RouteTableId("ipv4");
+    private static final RouteTableId IPV6 = new RouteTableId("ipv6");
+    private static final Logger log = LoggerFactory.getLogger(DistributedRouteStore.class);
+    private final SetEventListener<RouteTableId> masterRouteTableListener =
+            new MasterRouteTableListener();
+    private final RouteStoreDelegate ourDelegate = new InternalRouteStoreDelegate();
+
+    // Stores the route tables that have been created
+    private DistributedSet<RouteTableId> masterRouteTable;
+    // Local memory map to store route table object
+    private Map<RouteTableId, RouteTable> routeTables;
+
+    private ExecutorService executor;
+
+    public DistributedRouteStore(StorageService storageService) {
+        this.storageService = storageService;
+    }
+
+    /**
+     * Sets up distributed route store.
+     */
+    public void activate() {
+        routeTables = new ConcurrentHashMap<>();
+        executor = Executors.newSingleThreadExecutor(groupedThreads("onos/route", "store", log));
+
+        KryoNamespace masterRouteTableSerializer = KryoNamespace.newBuilder()
+                .register(RouteTableId.class)
+                .build();
+
+        masterRouteTable = storageService.<RouteTableId>setBuilder()
+                .withName("onos-master-route-table")
+                .withSerializer(Serializer.using(masterRouteTableSerializer))
+                .build()
+                .asDistributedSet();
+
+        masterRouteTable.addListener(masterRouteTableListener);
+
+        // Add default tables (add is idempotent)
+        masterRouteTable.add(IPV4);
+        masterRouteTable.add(IPV6);
+
+        masterRouteTable.forEach(this::createRouteTable);
+
+        log.info("Started");
+    }
+
+    /**
+     * Cleans up distributed route store.
+     */
+    public void deactivate() {
+        masterRouteTable.removeListener(masterRouteTableListener);
+
+        routeTables.values().forEach(RouteTable::shutdown);
+
+        log.info("Stopped");
+    }
+
+    @Override
+    public void updateRoute(Route route) {
+        getDefaultRouteTable(route).update(route);
+    }
+
+    @Override
+    public void removeRoute(Route route) {
+        getDefaultRouteTable(route).remove(route);
+    }
+
+    @Override
+    public Set<RouteTableId> getRouteTables() {
+        return ImmutableSet.copyOf(masterRouteTable);
+    }
+
+    @Override
+    public Collection<RouteSet> getRoutes(RouteTableId table) {
+        RouteTable routeTable = routeTables.get(table);
+        if (routeTable == null) {
+            return Collections.emptySet();
+        } else {
+            return ImmutableSet.copyOf(routeTable.getRoutes());
+        }
+    }
+
+    @Override
+    public Collection<Route> getRoutesForNextHop(IpAddress ip) {
+        return getDefaultRouteTable(ip).getRoutesForNextHop(ip);
+    }
+
+    @Override
+    public RouteSet getRoutes(IpPrefix prefix) {
+        return getDefaultRouteTable(prefix.address()).getRoutes(prefix);
+    }
+
+    private void createRouteTable(RouteTableId tableId) {
+        routeTables.computeIfAbsent(tableId, id -> new DefaultRouteTable(id, ourDelegate, storageService, executor));
+    }
+
+    private void destroyRouteTable(RouteTableId tableId) {
+        RouteTable table = routeTables.remove(tableId);
+        if (table != null) {
+            table.destroy();
+        }
+    }
+
+    private RouteTable getDefaultRouteTable(Route route) {
+        return getDefaultRouteTable(route.prefix().address());
+    }
+
+    private RouteTable getDefaultRouteTable(IpAddress ip) {
+        RouteTableId routeTableId = (ip.isIp4()) ? IPV4 : IPV6;
+        return routeTables.getOrDefault(routeTableId, EmptyRouteTable.instance());
+    }
+
+    private class InternalRouteStoreDelegate implements RouteStoreDelegate {
+        @Override
+        public void notify(InternalRouteEvent event) {
+            executor.execute(() -> DistributedRouteStore.this.notifyDelegate(event));
+        }
+    }
+
+    private class MasterRouteTableListener implements SetEventListener<RouteTableId> {
+        @Override
+        public void event(SetEvent<RouteTableId> event) {
+            switch (event.type()) {
+            case ADD:
+                executor.execute(() -> createRouteTable(event.entry()));
+                break;
+            case REMOVE:
+                executor.execute(() -> destroyRouteTable(event.entry()));
+                break;
+            default:
+                break;
+            }
+        }
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/EmptyRouteTable.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/EmptyRouteTable.java
new file mode 100644
index 0000000..3786675
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/EmptyRouteTable.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Route table that contains no routes.
+ */
+public final class EmptyRouteTable implements RouteTable {
+
+    private final RouteTableId id = new RouteTableId("empty");
+
+    private static final EmptyRouteTable INSTANCE = new EmptyRouteTable();
+
+    /**
+     * Returns the instance of the empty route table.
+     *
+     * @return empty route table
+     */
+    public static EmptyRouteTable instance() {
+        return INSTANCE;
+    }
+
+    private EmptyRouteTable() {
+    }
+
+    @Override
+    public void update(Route route) {
+
+    }
+
+    @Override
+    public void remove(Route route) {
+
+    }
+
+    @Override
+    public RouteTableId id() {
+        return id;
+    }
+
+    @Override
+    public Collection<RouteSet> getRoutes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public RouteSet getRoutes(IpPrefix prefix) {
+        return null;
+    }
+
+    @Override
+    public Collection<Route> getRoutesForNextHop(IpAddress nextHop) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public void shutdown() {
+
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/LocalRouteStore.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/LocalRouteStore.java
new file mode 100644
index 0000000..7884783
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/LocalRouteStore.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import com.googlecode.concurrenttrees.common.KeyValuePair;
+import com.googlecode.concurrenttrees.radix.node.concrete.DefaultByteArrayNodeFactory;
+import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree;
+import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.routeservice.InternalRouteEvent;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteStore;
+import org.onosproject.routeservice.RouteStoreDelegate;
+import org.onosproject.routeservice.RouteTableId;
+import org.onosproject.routeservice.RouteTools;
+import org.onosproject.store.AbstractStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Route store based on in-memory storage.
+ */
+public class LocalRouteStore extends AbstractStore<InternalRouteEvent, RouteStoreDelegate>
+        implements RouteStore {
+
+    private Logger log = LoggerFactory.getLogger(getClass());
+
+    private Map<RouteTableId, RouteTable> routeTables;
+    private static final RouteTableId IPV4 = new RouteTableId("ipv4");
+    private static final RouteTableId IPV6 = new RouteTableId("ipv6");
+
+    /**
+     * Sets up local route store.
+     */
+    public void activate() {
+        routeTables = new ConcurrentHashMap<>();
+
+        routeTables.put(IPV4, new RouteTable(IPV4));
+        routeTables.put(IPV6, new RouteTable(IPV6));
+
+        log.info("Started");
+    }
+
+    /**
+     * Cleans up local route store.
+     */
+    public void deactivate() {
+        log.info("Stopped");
+    }
+
+    @Override
+    public void updateRoute(Route route) {
+        getDefaultRouteTable(route).update(route);
+    }
+
+    @Override
+    public void removeRoute(Route route) {
+        getDefaultRouteTable(route).remove(route);
+    }
+
+    @Override
+    public Set<RouteTableId> getRouteTables() {
+        return routeTables.keySet();
+    }
+
+    @Override
+    public Collection<RouteSet> getRoutes(RouteTableId table) {
+        RouteTable routeTable = routeTables.get(table);
+        if (routeTable != null) {
+            return routeTable.getRouteSets();
+        }
+        return null;
+    }
+
+    @Override
+    public Collection<Route> getRoutesForNextHop(IpAddress ip) {
+        return getDefaultRouteTable(ip).getRoutesForNextHop(ip);
+    }
+
+    @Override
+    public RouteSet getRoutes(IpPrefix prefix) {
+        return getDefaultRouteTable(prefix.address()).getRoutes(prefix);
+    }
+
+    private RouteTable getDefaultRouteTable(Route route) {
+        return getDefaultRouteTable(route.prefix().address());
+    }
+
+    private RouteTable getDefaultRouteTable(IpAddress ip) {
+        RouteTableId routeTableId = (ip.isIp4()) ? IPV4 : IPV6;
+        return routeTables.get(routeTableId);
+    }
+
+    /**
+     * Route table into which routes can be placed.
+     */
+    private class RouteTable {
+        private final InvertedRadixTree<Route> routeTable;
+        private final Map<IpPrefix, Route> routes = new ConcurrentHashMap<>();
+        private final RouteTableId id;
+
+        /**
+         * Creates a new route table.
+         */
+        public RouteTable(RouteTableId id) {
+            this.id = checkNotNull(id);
+            routeTable = new ConcurrentInvertedRadixTree<>(
+                    new DefaultByteArrayNodeFactory());
+        }
+
+        /**
+         * Adds or updates the route in the route table.
+         *
+         * @param route route to update
+         */
+        public void update(Route route) {
+            synchronized (this) {
+                Route oldRoute = routes.put(route.prefix(), route);
+
+                // No need to proceed if the new route is the same
+                if (route.equals(oldRoute)) {
+                    return;
+                }
+
+                routeTable.put(RouteTools.createBinaryString(route.prefix()), route);
+
+                notifyDelegate(new InternalRouteEvent(
+                        InternalRouteEvent.Type.ROUTE_ADDED, singletonRouteSet(route)));
+            }
+        }
+
+        /**
+         * Removes the route from the route table.
+         *
+         * @param route route to remove
+         */
+        public void remove(Route route) {
+            synchronized (this) {
+                Route removed = routes.remove(route.prefix());
+                routeTable.remove(RouteTools.createBinaryString(route.prefix()));
+
+                if (removed != null) {
+                    notifyDelegate(new InternalRouteEvent(
+                            InternalRouteEvent.Type.ROUTE_REMOVED, emptyRouteSet(route.prefix())));
+                }
+            }
+        }
+
+        /**
+         * Returns the routes pointing to a particular next hop.
+         *
+         * @param ip next hop IP address
+         * @return routes for the next hop
+         */
+        public Collection<Route> getRoutesForNextHop(IpAddress ip) {
+            return routes.values()
+                    .stream()
+                    .filter(route -> route.nextHop().equals(ip))
+                    .collect(Collectors.toSet());
+        }
+
+        public RouteSet getRoutes(IpPrefix prefix) {
+            Route route = routes.get(prefix);
+            if (route != null) {
+                return singletonRouteSet(route);
+            }
+            return null;
+        }
+
+        public Collection<RouteSet> getRouteSets() {
+            return routes.values().stream()
+                    .map(this::singletonRouteSet)
+                    .collect(Collectors.toSet());
+        }
+
+        /**
+         * Returns all routes in the route table.
+         *
+         * @return all routes
+         */
+        public Collection<Route> getRoutes() {
+            Iterator<KeyValuePair<Route>> it =
+                    routeTable.getKeyValuePairsForKeysStartingWith("").iterator();
+
+            List<Route> routes = new LinkedList<>();
+
+            while (it.hasNext()) {
+                KeyValuePair<Route> entry = it.next();
+                routes.add(entry.getValue());
+            }
+
+            return routes;
+        }
+
+        /**
+         * Performs a longest prefix match with the given IP in the route table.
+         *
+         * @param ip IP address to look up
+         * @return most specific prefix containing the given
+         */
+        public Route longestPrefixMatch(IpAddress ip) {
+            Iterable<Route> prefixes =
+                    routeTable.getValuesForKeysPrefixing(RouteTools.createBinaryString(ip.toIpPrefix()));
+
+            Iterator<Route> it = prefixes.iterator();
+
+            Route route = null;
+            while (it.hasNext()) {
+                route = it.next();
+            }
+
+            return route;
+        }
+
+        private RouteSet singletonRouteSet(Route route) {
+            return new RouteSet(id, route.prefix(), Collections.singleton(route));
+        }
+
+        private RouteSet emptyRouteSet(IpPrefix prefix) {
+            return new RouteSet(id, prefix, Collections.emptySet());
+        }
+    }
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteStoreImpl.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteStoreImpl.java
new file mode 100644
index 0000000..5c18b54
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteStoreImpl.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.routeservice.InternalRouteEvent;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteStore;
+import org.onosproject.routeservice.RouteStoreDelegate;
+import org.onosproject.routeservice.RouteTableId;
+import org.onosproject.store.AbstractStore;
+import org.onosproject.store.service.StorageService;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Set;
+
+/**
+ * An implementation of RouteStore that is backed by either LocalRouteStore or
+ * DistributedRouteStore according to configuration.
+ */
+@Service
+@Component
+public class RouteStoreImpl extends AbstractStore<InternalRouteEvent, RouteStoreDelegate>
+        implements RouteStore {
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService componentConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    public StorageService storageService;
+
+    @Property(name = "distributed", boolValue = false,
+            label = "Enable distributed route store")
+    private boolean distributed;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private RouteStore currentRouteStore;
+
+    private DistributedRouteStore distributedRouteStore;
+    private LocalRouteStore localRouteStore;
+
+    @Activate
+    public void activate(ComponentContext context) {
+        distributedRouteStore = new DistributedRouteStore(storageService);
+        distributedRouteStore.activate();
+        localRouteStore = new LocalRouteStore();
+        localRouteStore.activate();
+
+        componentConfigService.registerProperties(getClass());
+        modified(context);
+    }
+
+    @Deactivate
+    public void deactivate() {
+        localRouteStore.deactivate();
+        distributedRouteStore.deactivate();
+
+        componentConfigService.unregisterProperties(getClass(), false);
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context.getProperties();
+        if (properties == null) {
+            return;
+        }
+
+        String strDistributed = Tools.get(properties, "distributed");
+        boolean expectDistributed = Boolean.parseBoolean(strDistributed);
+
+        // Start route store during first start or config change
+        // NOTE: new route store will be empty
+        if (currentRouteStore == null || expectDistributed != distributed) {
+            if (expectDistributed) {
+                currentRouteStore = distributedRouteStore;
+            } else {
+                currentRouteStore = localRouteStore;
+            }
+
+            this.distributed = expectDistributed;
+            log.info("Switched to {} route store", distributed ? "distributed" : "local");
+        }
+
+    }
+
+    @Override
+    public void setDelegate(RouteStoreDelegate delegate) {
+        super.setDelegate(delegate);
+
+        // Set the delegate of underlying route store implementations
+        localRouteStore.setDelegate(delegate);
+        distributedRouteStore.setDelegate(delegate);
+    }
+
+    @Override
+    public void unsetDelegate(RouteStoreDelegate delegate) {
+        super.unsetDelegate(delegate);
+
+        // Unset the delegate of underlying route store implementations
+        localRouteStore.unsetDelegate(delegate);
+        distributedRouteStore.unsetDelegate(delegate);
+    }
+
+    @Override
+    public void updateRoute(Route route) {
+        currentRouteStore.updateRoute(route);
+    }
+
+    @Override
+    public void removeRoute(Route route) {
+        currentRouteStore.removeRoute(route);
+    }
+
+    @Override
+    public Set<RouteTableId> getRouteTables() {
+        return currentRouteStore.getRouteTables();
+    }
+
+    @Override
+    public Collection<RouteSet> getRoutes(RouteTableId table) {
+        return currentRouteStore.getRoutes(table);
+    }
+
+    @Override
+    public Collection<Route> getRoutesForNextHop(IpAddress ip) {
+        return currentRouteStore.getRoutesForNextHop(ip);
+    }
+
+    @Override
+    public RouteSet getRoutes(IpPrefix prefix) {
+        return currentRouteStore.getRoutes(prefix);
+    }
+
+    @Override
+    public String name() {
+        return currentRouteStore.name();
+    }
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteTable.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteTable.java
new file mode 100644
index 0000000..fbe32c0
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/RouteTable.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.store;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteSet;
+import org.onosproject.routeservice.RouteTableId;
+
+import java.util.Collection;
+
+/**
+ * Represents a route table that stores routes.
+ */
+public interface RouteTable {
+
+    /**
+     * Adds a route to the route table.
+     *
+     * @param route route
+     */
+    void update(Route route);
+
+    /**
+     * Removes a route from the route table.
+     *
+     * @param route route
+     */
+    void remove(Route route);
+
+    /**
+     * Returns the route table ID.
+     *
+     * @return route table ID
+     */
+    RouteTableId id();
+
+    /**
+     * Returns all routes in the route table.
+     *
+     * @return collection of routes, grouped by prefix
+     */
+    Collection<RouteSet> getRoutes();
+
+    /**
+     * Returns the routes in this table pertaining to a given prefix.
+     *
+     * @param prefix IP prefix
+     * @return routes for the prefix
+     */
+    RouteSet getRoutes(IpPrefix prefix);
+
+    /**
+     * Returns all routes that have the given next hop.
+     *
+     * @param nextHop next hop IP address
+     * @return collection of routes
+     */
+    Collection<Route> getRoutesForNextHop(IpAddress nextHop);
+
+    /**
+     * Releases route table resources held locally.
+     */
+    void shutdown();
+
+    /**
+     * Releases route table resources across the entire cluster.
+     */
+    void destroy();
+
+}
diff --git a/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/package-info.java b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/package-info.java
new file mode 100644
index 0000000..fbf31a3
--- /dev/null
+++ b/apps/route-service/app/src/main/java/org/onosproject/routeservice/store/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Implementation of the unicast routing service.
+ */
+package org.onosproject.routeservice.store;
diff --git a/apps/route-service/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/route-service/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
new file mode 100644
index 0000000..a020c3b
--- /dev/null
+++ b/apps/route-service/app/src/main/resources/OSGI-INF/blueprint/shell-config.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright 2014-present Open Networking Foundation
+  ~
+  ~ 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.
+  -->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
+
+        <command>
+            <action class="org.onosproject.routeservice.cli.RoutesListCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.routeservice.cli.RouteAddCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.routeservice.cli.RouteRemoveCommand"/>
+        </command>
+        <command>
+            <action class="org.onosproject.routeservice.cli.RouteStoreCommand"/>
+        </command>
+
+    </command-bundle>
+
+</blueprint>
diff --git a/apps/route-service/app/src/test/java/org/onosproject/routeservice/impl/RouteManagerTest.java b/apps/route-service/app/src/test/java/org/onosproject/routeservice/impl/RouteManagerTest.java
new file mode 100644
index 0000000..8febe68
--- /dev/null
+++ b/apps/route-service/app/src/test/java/org/onosproject/routeservice/impl/RouteManagerTest.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+package org.onosproject.routeservice.impl;
+
+import java.util.Collections;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
+import org.onlab.packet.Ip6Address;
+import org.onlab.packet.Ip6Prefix;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.IpPrefix;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.routeservice.ResolvedRoute;
+import org.onosproject.routeservice.Route;
+import org.onosproject.routeservice.RouteEvent;
+import org.onosproject.routeservice.RouteListener;
+import org.onosproject.routeservice.store.LocalRouteStore;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultHost;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.host.HostServiceAdapter;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WorkQueue;
+
+import com.google.common.collect.Sets;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * Unit tests for the route manager.
+ */
+public class RouteManagerTest {
+
+    private static final ConnectPoint CP1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000001"),
+            PortNumber.portNumber(1));
+
+    private static final IpPrefix V4_PREFIX1 = Ip4Prefix.valueOf("1.1.1.0/24");
+    private static final IpPrefix V4_PREFIX2 = Ip4Prefix.valueOf("2.2.2.0/24");
+    private static final IpPrefix V6_PREFIX1 = Ip6Prefix.valueOf("4000::/64");
+
+    private static final IpAddress V4_NEXT_HOP1 = Ip4Address.valueOf("192.168.10.1");
+    private static final IpAddress V4_NEXT_HOP2 = Ip4Address.valueOf("192.168.20.1");
+    private static final IpAddress V6_NEXT_HOP1 = Ip6Address.valueOf("1000::1");
+    private static final IpAddress V6_NEXT_HOP2 = Ip6Address.valueOf("2000::1");
+
+    private static final MacAddress MAC1 = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final MacAddress MAC2 = MacAddress.valueOf("00:00:00:00:00:02");
+    private static final MacAddress MAC3 = MacAddress.valueOf("00:00:00:00:00:03");
+    private static final MacAddress MAC4 = MacAddress.valueOf("00:00:00:00:00:04");
+
+    private HostService hostService;
+
+    private RouteListener routeListener;
+    private HostListener hostListener;
+
+    private RouteManager routeManager;
+
+    @Before
+    public void setUp() throws Exception {
+        setUpHostService();
+
+        routeListener = createMock(RouteListener.class);
+
+        routeManager = new TestRouteManager();
+        routeManager.hostService = hostService;
+
+        routeManager.clusterService = createNiceMock(ClusterService.class);
+        replay(routeManager.clusterService);
+        routeManager.storageService = createNiceMock(StorageService.class);
+        expect(routeManager.storageService.getWorkQueue(anyString(), anyObject()))
+                .andReturn(createNiceMock(WorkQueue.class));
+        replay(routeManager.storageService);
+
+        LocalRouteStore routeStore = new LocalRouteStore();
+        routeStore.activate();
+        routeManager.routeStore = routeStore;
+        routeManager.activate();
+
+        routeManager.addListener(routeListener);
+    }
+
+    /**
+     * Sets up the host service with details of some hosts.
+     */
+    private void setUpHostService() {
+        hostService = createMock(HostService.class);
+
+        hostService.addListener(anyObject(HostListener.class));
+        expectLastCall().andDelegateTo(new TestHostService()).anyTimes();
+
+        Host host1 = createHost(MAC1, V4_NEXT_HOP1);
+        expectHost(host1);
+
+        Host host2 = createHost(MAC2, V4_NEXT_HOP2);
+        expectHost(host2);
+
+        Host host3 = createHost(MAC3, V6_NEXT_HOP1);
+        expectHost(host3);
+
+        Host host4 = createHost(MAC4, V6_NEXT_HOP2);
+        expectHost(host4);
+
+        replay(hostService);
+    }
+
+    /**
+     * Sets expectations on the host service for a given host.
+     *
+     * @param host host
+     */
+    private void expectHost(Host host) {
+        // Assume the host only has one IP address
+        IpAddress ip = host.ipAddresses().iterator().next();
+
+        expect(hostService.getHostsByIp(ip))
+                .andReturn(Sets.newHashSet(host)).anyTimes();
+        hostService.startMonitoringIp(ip);
+        expectLastCall().anyTimes();
+    }
+
+    /**
+     * Creates a host with the given parameters.
+     *
+     * @param macAddress MAC address
+     * @param ipAddress IP address
+     * @return new host
+     */
+    private Host createHost(MacAddress macAddress, IpAddress ipAddress) {
+        return new DefaultHost(ProviderId.NONE, HostId.NONE, macAddress,
+                VlanId.NONE, new HostLocation(CP1, 1),
+                Sets.newHashSet(ipAddress));
+    }
+
+    /**
+     * Adds a route to the route service without expecting any specific events
+     * to be sent to the route listeners.
+     *
+     * @param route route to add
+     */
+    private void addRoute(Route route) {
+        reset(routeListener);
+
+        routeListener.event(anyObject(RouteEvent.class));
+        expectLastCall().once();
+        replay(routeListener);
+
+        routeManager.update(Collections.singleton(route));
+
+        reset(routeListener);
+    }
+
+    /**
+     * Tests adding routes to the route manager.
+     */
+    @Test
+    public void testRouteAdd() {
+        Route route = new Route(Route.Source.STATIC, V4_PREFIX1, V4_NEXT_HOP1);
+        ResolvedRoute resolvedRoute = new ResolvedRoute(route, MAC1, CP1);
+
+        verifyRouteAdd(route, resolvedRoute);
+
+        route = new Route(Route.Source.STATIC, V6_PREFIX1, V6_NEXT_HOP1);
+        resolvedRoute = new ResolvedRoute(route, MAC3, CP1);
+
+        verifyRouteAdd(route, resolvedRoute);
+    }
+
+    /**
+     * Tests adding a new route and verifies that the correct event was sent
+     * to the route listener.
+     *
+     * @param route route to add
+     * @param resolvedRoute resolved route that should be sent to the route
+     *                      listener
+     */
+    private void verifyRouteAdd(Route route, ResolvedRoute resolvedRoute) {
+        reset(routeListener);
+
+        routeListener.event(event(RouteEvent.Type.ROUTE_ADDED, resolvedRoute));
+
+        replay(routeListener);
+
+        routeManager.update(Collections.singleton(route));
+
+        verify(routeListener);
+    }
+
+    /**
+     * Tests updating routes in the route manager.
+     */
+    @Test
+    public void testRouteUpdate() {
+        Route route = new Route(Route.Source.STATIC, V4_PREFIX1, V4_NEXT_HOP1);
+        Route updatedRoute = new Route(Route.Source.STATIC, V4_PREFIX1, V4_NEXT_HOP2);
+        ResolvedRoute resolvedRoute = new ResolvedRoute(route, MAC1, CP1);
+        ResolvedRoute updatedResolvedRoute = new ResolvedRoute(updatedRoute, MAC2, CP1);
+
+        verifyRouteUpdated(route, updatedRoute, resolvedRoute, updatedResolvedRoute);
+
+        // Different prefix pointing to the same next hop.
+        // In this case we expect to receive a ROUTE_UPDATED event.
+        route = new Route(Route.Source.STATIC, V4_PREFIX2, V4_NEXT_HOP1);
+        updatedRoute = new Route(Route.Source.STATIC, V4_PREFIX2, V4_NEXT_HOP2);
+        resolvedRoute = new ResolvedRoute(route, MAC1, CP1);
+        updatedResolvedRoute = new ResolvedRoute(updatedRoute, MAC2, CP1);
+
+        verifyRouteUpdated(route, updatedRoute, resolvedRoute, updatedResolvedRoute);
+
+        route = new Route(Route.Source.STATIC, V6_PREFIX1, V6_NEXT_HOP1);
+        updatedRoute = new Route(Route.Source.STATIC, V6_PREFIX1, V6_NEXT_HOP2);
+        resolvedRoute = new ResolvedRoute(route, MAC3, CP1);
+        updatedResolvedRoute = new ResolvedRoute(updatedRoute, MAC4, CP1);
+
+        verifyRouteUpdated(route, updatedRoute, resolvedRoute, updatedResolvedRoute);
+    }
+
+    /**
+     * Tests updating a route and verifies that the route listener receives a
+     * route updated event.
+     *
+     * @param original original route
+     * @param updated updated route
+     * @param resolvedRoute resolved route before update
+     * @param updatedResolvedRoute resolved route that is expected to be sent to
+     *                             the route listener
+     */
+    private void verifyRouteUpdated(Route original, Route updated,
+                                    ResolvedRoute resolvedRoute,
+                                    ResolvedRoute updatedResolvedRoute) {
+        // First add the original route
+        addRoute(original);
+
+        routeListener.event(event(RouteEvent.Type.ROUTE_UPDATED,
+                updatedResolvedRoute, resolvedRoute));
+        expectLastCall().once();
+
+        replay(routeListener);
+
+        routeManager.update(Collections.singleton(updated));
+
+        verify(routeListener);
+    }
+
+    /**
+     * Tests deleting routes from the route manager.
+     */
+    @Test
+    public void testRouteDelete() {
+        Route route = new Route(Route.Source.STATIC, V4_PREFIX1, V4_NEXT_HOP1);
+        ResolvedRoute removedResolvedRoute = new ResolvedRoute(route, MAC1, CP1);
+
+        verifyDelete(route, removedResolvedRoute);
+
+        route = new Route(Route.Source.STATIC, V6_PREFIX1, V6_NEXT_HOP1);
+        removedResolvedRoute = new ResolvedRoute(route, MAC3, CP1);
+
+        verifyDelete(route, removedResolvedRoute);
+    }
+
+    /**
+     * Tests deleting a route and verifies that the correct event is sent to
+     * the route listener.
+     *
+     * @param route route to delete
+     * @param removedResolvedRoute the resolved route being removed
+     */
+    private void verifyDelete(Route route, ResolvedRoute removedResolvedRoute) {
+        addRoute(route);
+
+        RouteEvent withdrawRouteEvent = new RouteEvent(RouteEvent.Type.ROUTE_REMOVED,
+                removedResolvedRoute);
+
+        reset(routeListener);
+        routeListener.event(withdrawRouteEvent);
+
+        replay(routeListener);
+
+        routeManager.withdraw(Collections.singleton(route));
+
+        verify(routeListener);
+    }
+
+    /**
+     * Tests adding a route entry where the HostService does not immediately
+     * know the MAC address of the next hop, but this is learnt later.
+     */
+    @Test
+    public void testAsyncRouteAdd() {
+        Route route = new Route(Route.Source.STATIC, V4_PREFIX1, V4_NEXT_HOP1);
+
+        // Host service will reply with no hosts when asked
+        reset(hostService);
+        expect(hostService.getHostsByIp(anyObject(IpAddress.class))).andReturn(
+                Collections.emptySet()).anyTimes();
+        hostService.startMonitoringIp(V4_NEXT_HOP1);
+        expectLastCall().anyTimes();
+        replay(hostService);
+
+        // Initially when we add the route, no route event will be sent because
+        // the host is not known
+        replay(routeListener);
+
+        routeManager.update(Collections.singleton(route));
+
+        verify(routeListener);
+
+        // Now when we send the event, we expect the FIB update to be sent
+        reset(routeListener);
+        routeListener.event(event(RouteEvent.Type.ROUTE_ADDED,
+                new ResolvedRoute(route, MAC1, CP1)));
+        replay(routeListener);
+
+        Host host = createHost(MAC1, V4_NEXT_HOP1);
+
+        // Set up the host service with a host
+        reset(hostService);
+        expect(hostService.getHostsByIp(V4_NEXT_HOP1)).andReturn(
+                Collections.singleton(host)).anyTimes();
+        hostService.startMonitoringIp(V4_NEXT_HOP1);
+        expectLastCall().anyTimes();
+        replay(hostService);
+
+        // Send in the host event
+        hostListener.event(new HostEvent(HostEvent.Type.HOST_ADDED, host));
+
+        verify(routeListener);
+    }
+
+    private static RouteEvent event(RouteEvent.Type type, ResolvedRoute subject) {
+        return event(type, subject, null);
+    }
+
+    private static RouteEvent event(RouteEvent.Type type, ResolvedRoute subject, ResolvedRoute prevSubject) {
+        return new RouteEvent(type, subject, prevSubject, Collections.singleton(subject));
+    }
+
+    /**
+     * Test host service that stores a reference to the host listener.
+     */
+    private class TestHostService extends HostServiceAdapter {
+        @Override
+        public void addListener(HostListener listener) {
+            hostListener = listener;
+        }
+    }
+
+    /**
+     * Test route manager that extends the real route manager and injects a test
+     * listener queue instead of the real listener queue.
+     */
+    private static class TestRouteManager extends RouteManager {
+        @Override
+        ListenerQueue createListenerQueue(RouteListener listener) {
+            return new TestListenerQueue(listener);
+        }
+    }
+
+    /**
+     * Test listener queue that simply passes route events straight through to
+     * the route listener on the caller's thread.
+     */
+    private static class TestListenerQueue implements ListenerQueue {
+
+        private final RouteListener listener;
+
+        public TestListenerQueue(RouteListener listener) {
+            this.listener = listener;
+        }
+
+        @Override
+        public void post(RouteEvent event) {
+            listener.event(event);
+        }
+
+        @Override
+        public void start() {
+        }
+
+        @Override
+        public void stop() {
+        }
+    }
+
+}