Repository: eclipse/leshan
Pull request #113 (2016-04-01 15:52:08)

Add clustering support (based on Redis)
This PR aim to allow to deploy [Leshan in a Cluster](https://github.com/eclipse/leshan/wiki/Cluster).

The first step was to add  `ClientRegistry`, `SecurityRegistry` and `ObservationRegistry` implementation based on [Redis](http://redis.io/) to Leshan Server Demo. (Previously done in #89  and #93)

The Redis `ObservationRegistry` implementation is based on the californium [PR #16](https://github.com/eclipse/californium/pull/16). So you need to get it and build it locally to test it.

**This work is still in progress.**





ingoschaal: Hi Julien
based on the discussion in PR#89 I have a suggestion:
I fully agree with you to keep leshan-core as a pure LWM2M protocol library, without clustering. 
But what about those who do not use the demo server , rather use their own server implementation.
Perhaps we could establish a new Cluster Layer which could used from demo serve and all other server implementations, but not have to.
ingoschaal: Hello everybody
are there any news regarding this two impediments?

CQ under review for jedis + common-pool2
http://dev.eclipse.org/ipzilla/show_bug.cgi?id=11027
http://dev.eclipse.org/ipzilla/show_bug.cgi?id=11029

otherwise we would be pleased if this PR is merged into the master. 
We have had good experience and think that it is a good basis for proposed [Cluster](https://github.com/eclipse/leshan/wiki/Cluster)
jvermillard: Thanks @ingoschaal for the feedback!
The CQ are validated. Now we are waiting for https://github.com/eclipse/californium/pull/16 merge & release in californium.
ingoschaal: Another question:
The consistency concept of Redis is eventual consistency. This means that the write operation is not directly propagated to all replicas.
The KeyToken which is generated in Californium Matcher should be unique to the server. 
What happens when the same token is generated to be persisted on a cluster which is not synchronized at this thime? 
Is redis at this point the right choice?
Or concerns this Problem only [PR #16](https://github.com/eclipse/californium/pull/16)
mhemmeter: Hi @jvermillard and others,

regarding the upcoming merge into the master branch: Currently all LWM2M Servers that are based on Eclipse Leshan but have their own server implementation would need to include the demo server in order to use the new clustering feature. That's something I would like to avoid in our product and therefore I would propose to introduce a new module _leshan-server-cluster_ (as an alternative one could also introduce a more generic module for all upcoming extensions of the core library, something like _leshan-server-extensions_).

What do you and others think? If everybody agrees we can provide a PR for that.

Thanks
Mathias
sbernard31: @mhemmeter,
I'm glad to see that you want to avoid to depend on leshan-server-demo, We take a lot of time to explain that leshan-server-demo is not intended to be directly reused. It's just a demo/example :)

Currently, we put all redis implementation in leshan-server-demo because this is still an experimental works, but we aims to make this reusable and so this will not stay in leshan-server-demo for ever ;) !
sophokles73: @sbernard31  I see you point and in general I would agree that the Redis stuff is still *experimental*. However, we could make it easier for people like @mhemmeter to test (end evolve) the code by putting it into a separate module that would not require them to depend on the demo server.

We can still put a disclaimer in there stating that this module is considered experimental ...
sbernard31: @ingoschaal  about the token issue.
In some [rare cases](http://redis.io/topics/cluster-tutorial#redis-cluster-consistency-guarantees), we can lost a token and so an observation. This is a trade-off between performance and consistency.
We will not be able to provide an implementation which covers all needs that's why we define interfaces to let users free to use adapted technology.

@sophokles73 I'm not sure to see the difference between depends on `leshan-server-demo` or `leshen-server-experimental` ;)

sophokles73: That's why I wouldn't call it `leshan-server-experimental` but put it in the right module from the beginning, e.g. `leshan-server-clustering` or whatever :-)
jvermillard: IMO the leshan maven modules already reached an unbearable level of over engineering for me :)
20k SLOC  and 9 modules.

Before introducing more module I would like to explore if we can merge some.

Since the main proponents of having californium  replaceable are gone I think we could merge: 
 * leshan-server-core and leshan-server-cf
 * leshan-client-core and leshan-client-cf
sophokles73: I have no problem at all with merging them.
I also do not think that we *need* a separate module for the clustering stuff. We can simply add it to a new package within *leshan-server-core* as well, I guess.

Just out of curiosity: who is the party that was proposing the split up and that is now *gone*? Did I miss something?
mhemmeter: @sophokles73: In  PR #89 it has been discussed that the _leshan-server-core_ module is currently seen as a pure LWM2M protocol library and therefore the clustering feature was implemented as part of the demo server. As I can follow this argument I proposed to create a new module. But I would also be fine if it is included in the core in a separate package. My only wish is that it is not in the demo server :-) 

Background of the request is that we are planning to invest in making the clustering feature production ready in the next months. So I would like to start having it in the right module from the beginning. `leshan-server-clustering` sounds good to me if it becomes a separate module.
sbernard31: I'm not sure the among of modules is a real problem.
The module separation is pretty simple : https://github.com/eclipse/leshan#modules
On the 9 module there is only 5 real modules (the others are just for tests and demos)

About merging cf and core, It would be a shame to lost the abstraction we introduce between LWM2M and transport layer. It could help us to implement LWM2M over other implementation or transport layer (CoAP/TCP).

About clustering, I'm not ok to create a leshan-server-clustering now, because I'm currently not able  to know if it's the right way to separate the code (for now we have only 2 classes to put on it ...). I suppose we can  move this 2 redis implementation from demo to core (with redis optional depency on server-core), but this will not make it less experimental. (sweeping dust under the carpet?)


jpradocueva: Just for information.
OMA is developing OMA LwM2M v1.1 where one of the new features is CoAP over TCP.
I can prepare a summary of what is planned for OMA LwM2M v1.1 and post it in the distribution list if it helps.
jvermillard: > About merging cf and core, It would be a shame to lost the abstraction we introduce between LWM2M and transport layer. It could help us to implement LWM2M over other implementation or transport layer (CoAP/TCP).
It's not about removing the abstraction, just keeping everything in one module, you are free to not use the Cf impl.
I think I'm a bit out of topic here :)
mhemmeter: Seems like there is a veto from @sbernard31 for creating a separate clustering module. So what about moving the clustering implementation to `leshan-server-core`? If there are no objections we will create the pull request.
sophokles73: Ok, here's what I propose:

* we create a module `leshan-server-clustering` in which create and evolve the clustering support. From my point of view it is totally acceptable to have code in there that *is not (yet) perfect* (in the end, which of our code is?).
* indedpendently from that we can discuss which (other) modules to merge. I see the value of keeping the basic modules clean of dependencies that are only used for clustering, thus I prefer to have a separate module for it.

Patch:
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/LinkObject.java b/leshan-core/src/main/java/org/eclipse/leshan/LinkObject.java
index 8b877a89..7d060195 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/LinkObject.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/LinkObject.java
@@ -15,12 +15,11 @@
  *******************************************************************************/
 package org.eclipse.leshan;
 
+import java.io.Serializable;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import org.eclipse.leshan.util.Charsets;
 import org.eclipse.leshan.util.StringUtils;
@@ -30,18 +29,14 @@ import org.eclipse.leshan.util.StringUtils;
  */
 // TODO this class should not have a lwM2M flavor.
 // TODO we should have a look at org.eclipse.californium.core.coap.LinkFormat
-public class LinkObject {
+public class LinkObject implements Serializable {
+
+    private static final long serialVersionUID = 1L;
 
     private final String url;
 
     private final Map<String, Object> attributes;
 
-    private final Integer objectId;
-
-    private final Integer objectInstanceId;
-
-    private final Integer resourceId;
-
     /**
      * Creates a new link object without attributes.
      * 
@@ -64,18 +59,6 @@ public class LinkObject {
         } else {
             this.attributes = Collections.unmodifiableMap(new HashMap<String, Object>());
         }
-
-        Matcher mat = Pattern.compile("(/(\\d+))(/(\\d+))?(/(\\d+))?").matcher(url);
-
-        if (mat.find()) {
-            objectId = mat.group(2) == null ? null : new Integer(mat.group(2));
-            objectInstanceId = mat.group(4) == null ? null : new Integer(mat.group(4));
-            resourceId = mat.group(6) == null ? null : new Integer(mat.group(6));
-        } else {
-            objectId = null;
-            objectInstanceId = null;
-            resourceId = null;
-        }
     }
 
     public String getUrl() {
@@ -91,26 +74,6 @@ public class LinkObject {
         return attributes;
     }
 
-    // TODO Object id, instance id and resource id is not really a LinkObject concept
-    // we should remove this.
-    @Deprecated
-    public String getPath() {
-        StringBuilder sb = new StringBuilder("/");
-        if (objectId != null) {
-            sb.append(objectId);
-        }
-
-        if (objectInstanceId != null) {
-            sb.append("/").append(objectInstanceId);
-        }
-
-        if (resourceId != null) {
-            sb.append("/").append(resourceId);
-        }
-
-        return sb.toString();
-    }
-
     @Override
     public String toString() {
         final StringBuilder builder = new StringBuilder();
@@ -136,23 +99,6 @@ public class LinkObject {
         return builder.toString();
     }
 
-    // TODO Object id, instance id and resource id is not really a LinkObject concept
-    // we should remove this 3 methods below.
-    @Deprecated
-    public Integer getObjectId() {
-        return objectId;
-    }
-
-    @Deprecated
-    public Integer getObjectInstanceId() {
-        return objectInstanceId;
-    }
-
-    @Deprecated
-    public Integer getResourceId() {
-        return resourceId;
-    }
-
     public static LinkObject[] parse(byte[] content) {
         String s = new String(content, Charsets.UTF_8);
         String[] links = s.split(",");
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java
index ce7945c5..67bc599f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/observation/Observation.java
@@ -21,30 +21,42 @@ import org.eclipse.leshan.core.node.LwM2mPath;
  * An observation of a resource provided by a LWM2M Client.
  * 
  */
-public interface Observation {
+public class Observation {
+
+    private byte[] id;
+    private LwM2mPath path;
+    private String registrationId;
+
+    public Observation(byte[] id, String registrationId, LwM2mPath path) {
+        super();
+        this.id = id;
+        this.path = path;
+        this.registrationId = registrationId;
+    }
 
     /**
-     * Get the registration ID link to this observation.
+     * Get the id of this observation.
      * 
-     * @return the registration ID
      */
-    String getRegistrationId();
+    public byte[] getId() {
+        return id;
+    }
 
     /**
-     * Gets the observed resource path.
+     * Get the registration ID link to this observation.
      * 
-     * @return the resource path
+     * @return the registration ID
      */
-    LwM2mPath getPath();
+    public String getRegistrationId() {
+        return registrationId;
+    }
 
     /**
-     * Cancels the observation.
+     * Gets the observed resource path.
      * 
-     * As a result the observer will no longer get notified about changes to the resource.
+     * @return the resource path
      */
-    void cancel();
-
-    void addListener(ObservationListener listener);
-
-    void removeListener(ObservationListener listener);
+    public LwM2mPath getPath() {
+        return path;
+    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/ObservationListener.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/CaliforniumObservationRegistry.java
similarity index 53%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/observation/ObservationListener.java
rename to leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/CaliforniumObservationRegistry.java
index a108f5c7..aa519e1c 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/observation/ObservationListener.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/CaliforniumObservationRegistry.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * Copyright (c) 2016 Sierra Wireless and others.
  * 
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
@@ -13,13 +13,18 @@
  * Contributors:
  *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-package org.eclipse.leshan.core.observation;
+package org.eclipse.leshan.server.californium;
 
-import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.californium.core.observe.NotificationListener;
+import org.eclipse.californium.core.observe.ObservationStore;
+import org.eclipse.leshan.server.observation.ObservationRegistry;
 
-public interface ObservationListener {
+public interface CaliforniumObservationRegistry extends ObservationRegistry, NotificationListener {
 
-    void cancelled(Observation observation);
+    ObservationStore getObservationStore();
 
-    void newValue(Observation observation, LwM2mNode value);
+    void setNonSecureEndpoint(Endpoint endpoint);
+
+    void setSecureEndpoint(Endpoint endpoint);
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
index 7215e524..c6af011b 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
@@ -18,15 +18,15 @@ package org.eclipse.leshan.server.californium;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
+import org.eclipse.californium.core.observe.InMemoryObservationStore;
 import org.eclipse.leshan.server.LwM2mServer;
+import org.eclipse.leshan.server.californium.impl.CaliforniumObservationRegistryImpl;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
 import org.eclipse.leshan.server.client.ClientRegistry;
 import org.eclipse.leshan.server.impl.ClientRegistryImpl;
-import org.eclipse.leshan.server.impl.ObservationRegistryImpl;
 import org.eclipse.leshan.server.impl.SecurityRegistryImpl;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.model.StandardModelProvider;
-import org.eclipse.leshan.server.observation.ObservationRegistry;
 import org.eclipse.leshan.server.security.SecurityRegistry;
 
 /**
@@ -43,7 +43,7 @@ public class LeshanServerBuilder {
     public static final int PORT_DTLS = 5684;
 
     private SecurityRegistry securityRegistry;
-    private ObservationRegistry observationRegistry;
+    private CaliforniumObservationRegistry observationRegistry;
     private ClientRegistry clientRegistry;
     private LwM2mModelProvider modelProvider;
     private InetSocketAddress localAddress;
@@ -82,7 +82,7 @@ public class LeshanServerBuilder {
         return this;
     }
 
-    public LeshanServerBuilder setObservationRegistry(ObservationRegistry observationRegistry) {
+    public LeshanServerBuilder setObservationRegistry(CaliforniumObservationRegistry observationRegistry) {
         this.observationRegistry = observationRegistry;
         return this;
     }
@@ -106,12 +106,13 @@ public class LeshanServerBuilder {
             clientRegistry = new ClientRegistryImpl();
         if (securityRegistry == null)
             securityRegistry = new SecurityRegistryImpl();
-        if (observationRegistry == null)
-            observationRegistry = new ObservationRegistryImpl();
-        if (modelProvider == null) {
+        if (modelProvider == null)
             modelProvider = new StandardModelProvider();
-        }
-        return new LeshanServer(localAddress, localSecureAddress, clientRegistry, securityRegistry,
-                observationRegistry, modelProvider);
+        if (observationRegistry == null)
+            observationRegistry = new CaliforniumObservationRegistryImpl(new InMemoryObservationStore(), clientRegistry,
+                    modelProvider);
+
+        return new LeshanServer(localAddress, localSecureAddress, clientRegistry, securityRegistry, observationRegistry,
+                modelProvider);
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservation.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservation.java
deleted file mode 100644
index 9fa3d9d2..00000000
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservation.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
- * 
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Sierra Wireless - initial API and implementation
- *******************************************************************************/
-package org.eclipse.leshan.server.californium.impl;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.eclipse.californium.core.coap.CoAP;
-import org.eclipse.californium.core.coap.MessageObserverAdapter;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.leshan.core.model.LwM2mModel;
-import org.eclipse.leshan.core.node.LwM2mNode;
-import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.node.codec.InvalidValueException;
-import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
-import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.observation.ObservationListener;
-import org.eclipse.leshan.core.request.ContentFormat;
-import org.eclipse.leshan.util.Validate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public final class CaliforniumObservation extends MessageObserverAdapter implements Observation {
-    private final Logger LOG = LoggerFactory.getLogger(CaliforniumObservation.class);
-
-    private final Request coapRequest;
-    private final List<ObservationListener> listeners = new CopyOnWriteArrayList<>();
-    private final String registrationId;
-    private final LwM2mPath path;
-    private final LwM2mModel model;
-
-    public CaliforniumObservation(Request coapRequest, String registrationId, LwM2mPath path, LwM2mModel model) {
-        Validate.notNull(coapRequest);
-        Validate.notNull(registrationId);
-        Validate.notNull(path);
-        Validate.notNull(model);
-
-        this.coapRequest = coapRequest;
-        this.registrationId = registrationId;
-        this.path = path;
-        this.model = model;
-    }
-
-    @Override
-    public void cancel() {
-        coapRequest.cancel();
-    }
-
-    @Override
-    public void onResponse(Response coapResponse) {
-        // TODO remove the CHANGED test case, the spec say now a successful notify should be a 2.05 content
-        if (coapResponse.getCode() == CoAP.ResponseCode.CHANGED || coapResponse.getCode() == CoAP.ResponseCode.CONTENT) {
-            try {
-                LwM2mNode content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
-                        ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), path, model);
-                for (ObservationListener listener : listeners) {
-                    listener.newValue(this, content);
-                }
-            } catch (InvalidValueException e) {
-                String msg = String.format("[%s] ([%s])", e.getMessage(), e.getPath().toString());
-                LOG.debug(msg);
-            }
-        }
-    }
-
-    @Override
-    public void onCancel() {
-        for (ObservationListener listener : listeners) {
-            listener.cancelled(this);
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String getRegistrationId() {
-        return registrationId;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public LwM2mPath getPath() {
-        return path;
-    }
-
-    @Override
-    public String toString() {
-        return String.format("CaliforniumObservation [%s]", path);
-    }
-
-    @Override
-    public void addListener(ObservationListener listener) {
-        listeners.add(listener);
-
-    }
-
-    @Override
-    public void removeListener(ObservationListener listener) {
-        listeners.remove(listener);
-
-    }
-}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java
new file mode 100644
index 00000000..4170c0d4
--- /dev/null
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java
@@ -0,0 +1,219 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.californium.impl;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.californium.core.network.Exchange.KeyToken;
+import org.eclipse.californium.core.observe.NotificationListener;
+import org.eclipse.californium.core.observe.ObservationStore;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.codec.InvalidValueException;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.server.californium.CaliforniumObservationRegistry;
+import org.eclipse.leshan.server.client.Client;
+import org.eclipse.leshan.server.client.ClientRegistry;
+import org.eclipse.leshan.server.model.LwM2mModelProvider;
+import org.eclipse.leshan.server.observation.ObservationRegistry;
+import org.eclipse.leshan.server.observation.ObservationRegistryListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CaliforniumObservationRegistryImpl
+        implements CaliforniumObservationRegistry, ObservationRegistry, NotificationListener {
+
+    private final Logger LOG = LoggerFactory.getLogger(CaliforniumObservationRegistry.class);
+
+    private final ObservationStore observationStore;
+    private final ClientRegistry clientRegistry;
+    private final LwM2mModelProvider modelProvider;
+    private Endpoint secureEndpoint;
+    private Endpoint nonSecureEndpoint;
+
+    private final List<ObservationRegistryListener> listeners = new CopyOnWriteArrayList<>();
+    private final Map<KeyToken, Observation> observations = new ConcurrentHashMap<KeyToken, Observation>();
+
+    public CaliforniumObservationRegistryImpl(ObservationStore store, ClientRegistry clientRegistry,
+            LwM2mModelProvider modelProvider) {
+        this.observationStore = store;
+        this.modelProvider = modelProvider;
+        this.clientRegistry = clientRegistry;
+    }
+
+    @Override
+    public void addObservation(Observation observation) {
+        observations.put(new KeyToken(observation.getId()), observation);
+
+        for (ObservationRegistryListener listener : listeners) {
+            listener.newObservation(observation);
+        }
+    }
+
+    @Override
+    public void setNonSecureEndpoint(Endpoint endpoint) {
+        nonSecureEndpoint = endpoint;
+    }
+
+    @Override
+    public void setSecureEndpoint(Endpoint endpoint) {
+        secureEndpoint = endpoint;
+    }
+
+    @Override
+    public int cancelObservations(Client client) {
+        // check registration id
+        String registrationId = client.getRegistrationId();
+        if (registrationId == null)
+            return 0;
+
+        Set<Observation> observations = getObservations(registrationId);
+        for (Observation observation : observations) {
+            cancelObservation(observation);
+        }
+        return observations.size();
+    }
+
+    @Override
+    public int cancelObservations(Client client, String resourcepath) {
+        // check registration id
+        String registrationId = client.getRegistrationId();
+        if (registrationId == null || resourcepath == null || resourcepath.isEmpty())
+            return 0;
+
+        Set<Observation> observations = getObservations(registrationId, resourcepath);
+        for (Observation observation : observations) {
+            cancelObservation(observation);
+        }
+        return observations.size();
+    }
+
+    @Override
+    public void cancelObservation(Observation observation) {
+        if (observation == null)
+            return;
+
+        if (secureEndpoint != null)
+            secureEndpoint.cancelObservation(observation.getId());
+        if (nonSecureEndpoint != null)
+            nonSecureEndpoint.cancelObservation(observation.getId());
+        observations.remove(new KeyToken(observation.getId()));
+
+        for (ObservationRegistryListener listener : listeners) {
+            listener.cancelled(observation);
+        }
+    }
+
+    @Override
+    public Set<Observation> getObservations(Client client) {
+        return getObservations(client.getRegistrationId());
+    }
+
+    private Set<Observation> getObservations(String registrationId) {
+        if (registrationId == null)
+            return Collections.emptySet();
+
+        Set<Observation> result = new HashSet<Observation>();
+        for (Map.Entry<KeyToken, Observation> entry : observations.entrySet()) {
+            Observation observation = entry.getValue();
+            if (registrationId.equals(observation.getRegistrationId())) {
+                result.add(observation);
+            }
+        }
+        return result;
+    }
+
+    private Set<Observation> getObservations(String registrationId, String resourcePath) {
+        if (registrationId == null || resourcePath == null)
+            return Collections.emptySet();
+
+        Set<Observation> result = new HashSet<Observation>();
+        for (Observation observation : observations.values()) {
+            if (registrationId.equals(observation.getRegistrationId())
+                    && new LwM2mPath(resourcePath).equals(observation.getPath())) {
+                result.add(observation);
+            }
+        }
+        return result;
+    }
+
+    public ObservationStore getObservationStore() {
+        return observationStore;
+    }
+
+    @Override
+    public void addListener(ObservationRegistryListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(ObservationRegistryListener listener) {
+        listeners.remove(listener);
+    }
+
+    // ********** NotificationListener interface **********//
+
+    @Override
+    public void onNotification(Request coapRequest, Response coapResponse) {
+        if (listeners.isEmpty())
+            return;
+
+        if (coapResponse.getCode() == CoAP.ResponseCode.CHANGED
+                || coapResponse.getCode() == CoAP.ResponseCode.CONTENT) {
+            try {
+                // get observation for this request
+                Observation observation = observations.get(new KeyToken(coapResponse.getToken()));
+                if (observation == null)
+                    return;
+
+                // get client for this registration ID
+                Client client = clientRegistry.findByRegistrationId(observation.getRegistrationId());
+                if (client == null)
+                    // TODO Should we clean registrationIDs maps ?
+                    return;
+
+                // get model for this client
+                LwM2mModel model = modelProvider.getObjectModel(client);
+
+                // decode response
+                LwM2mNode content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
+                        ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), observation.getPath(),
+                        model);
+
+                // notify all listeners
+                for (ObservationRegistryListener listener : listeners) {
+                    listener.newValue(observation, content);
+                }
+            } catch (InvalidValueException e) {
+                String msg = String.format("[%s] ([%s])", e.getMessage(), e.getPath().toString());
+                LOG.debug(msg);
+            }
+        }
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
index 40b30ff6..9be4dfdd 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
@@ -44,6 +44,7 @@ import org.eclipse.leshan.server.Destroyable;
 import org.eclipse.leshan.server.LwM2mServer;
 import org.eclipse.leshan.server.Startable;
 import org.eclipse.leshan.server.Stoppable;
+import org.eclipse.leshan.server.californium.CaliforniumObservationRegistry;
 import org.eclipse.leshan.server.californium.LeshanServerBuilder;
 import org.eclipse.leshan.server.client.Client;
 import org.eclipse.leshan.server.client.ClientRegistry;
@@ -81,7 +82,7 @@ public class LeshanServer implements LwM2mServer {
 
     private final ClientRegistry clientRegistry;
 
-    private final ObservationRegistry observationRegistry;
+    private final CaliforniumObservationRegistry observationRegistry;
 
     private final SecurityRegistry securityRegistry;
 
@@ -103,7 +104,7 @@ public class LeshanServer implements LwM2mServer {
      */
     public LeshanServer(InetSocketAddress localAddress, InetSocketAddress localSecureAddress,
             final ClientRegistry clientRegistry, final SecurityRegistry securityRegistry,
-            final ObservationRegistry observationRegistry, final LwM2mModelProvider modelProvider) {
+            final CaliforniumObservationRegistry observationRegistry, final LwM2mModelProvider modelProvider) {
         Validate.notNull(localAddress, "IP address cannot be null");
         Validate.notNull(localSecureAddress, "Secure IP address cannot be null");
         Validate.notNull(clientRegistry, "clientRegistry cannot be null");
@@ -115,7 +116,6 @@ public class LeshanServer implements LwM2mServer {
         this.clientRegistry = clientRegistry;
         this.securityRegistry = securityRegistry;
         this.observationRegistry = observationRegistry;
-
         this.modelProvider = modelProvider;
 
         // Cancel observations on client unregistering
@@ -143,7 +143,10 @@ public class LeshanServer implements LwM2mServer {
                 return new RootResource();
             }
         };
-        nonSecureEndpoint = new CoapEndpoint(localAddress);
+        nonSecureEndpoint = new CoapEndpoint(localAddress, NetworkConfig.getStandard(),
+                this.observationRegistry.getObservationStore());
+        nonSecureEndpoint.addNotificationListener(observationRegistry);
+        observationRegistry.setNonSecureEndpoint(nonSecureEndpoint);
         coapServer.addEndpoint(nonSecureEndpoint);
 
         // secure endpoint
@@ -167,7 +170,10 @@ public class LeshanServer implements LwM2mServer {
             builder.setTrustStore(trustedCertificates);
         }
 
-        secureEndpoint = new CoapEndpoint(new DTLSConnector(builder.build()), NetworkConfig.getStandard());
+        secureEndpoint = new CoapEndpoint(new DTLSConnector(builder.build()), NetworkConfig.getStandard(),
+                this.observationRegistry.getObservationStore());
+        secureEndpoint.addNotificationListener(observationRegistry);
+        observationRegistry.setSecureEndpoint(secureEndpoint);
         coapServer.addEndpoint(secureEndpoint);
 
         // define /rd resource
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
index b066cda5..0e3f681a 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
@@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.codec.InvalidValueException;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapFinishRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
@@ -271,9 +272,8 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
             LwM2mNode content = decodeCoapResponse(request.getPath(), coapResponse);
             if (coapResponse.getOptions().hasObserve()) {
                 // observe request succeed so we can add and observation to registry
-                final CaliforniumObservation observation = new CaliforniumObservation(coapRequest,
-                        client.getRegistrationId(), request.getPath(), model);
-                coapRequest.addMessageObserver(observation);
+                Observation observation = new Observation(coapRequest.getToken(), client.getRegistrationId(),
+                        request.getPath());
                 observationRegistry.addObservation(observation);
                 // add the observation to an ObserveResponse instance
                 lwM2mresponse = new ObserveResponse(ResponseCode.CONTENT, content, observation, null);
diff --git a/leshan-server-cluster/pom.xml b/leshan-server-cluster/pom.xml
new file mode 100644
index 00000000..05edec05
--- /dev/null
+++ b/leshan-server-cluster/pom.xml
@@ -0,0 +1,86 @@
+<!--
+
+Copyright (c) 2013-2016 Sierra Wireless and others.
+
+All rights reserved. This program and the accompanying materials
+are made available under the terms of the Eclipse Public License v1.0
+and Eclipse Distribution License v1.0 which accompany this distribution.
+
+The Eclipse Public License is available at
+   http://www.eclipse.org/legal/epl-v10.html
+and the Eclipse Distribution License is available at
+   http://www.eclipse.org/org/documents/edl-v10.html.
+
+Contributors:
+    Sierra Wireless - initial API and implementation
+    Bosch Software Innovations GmbH - OSGi support
+
+-->
+<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>
+        <groupId>org.eclipse.leshan</groupId>
+        <artifactId>leshan</artifactId>
+        <version>0.1.11-M11-SNAPSHOT</version>
+    </parent>
+    <artifactId>leshan-server-cluster</artifactId>
+    <packaging>bundle</packaging>
+    <name>leshan - server cluster</name>
+    <description>Experimental clustering support for Leshan.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.leshan</groupId>
+            <artifactId>leshan-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.leshan</groupId>
+            <artifactId>leshan-server-cf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>2.8.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.eclipsesource.minimal-json</groupId>
+            <artifactId>minimal-json</artifactId>
+        </dependency>
+
+        <!-- runtime dependencies -->
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j-impl</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <_exportcontents>*</_exportcontents>
+                        <Import-Package>*</Import-Package>
+                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisClientRegistry.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisClientRegistry.java
new file mode 100644
index 00000000..fe76bbcd
--- /dev/null
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisClientRegistry.java
@@ -0,0 +1,286 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.californium.scandium.util.ByteArrayUtils;
+import org.eclipse.leshan.server.client.Client;
+import org.eclipse.leshan.server.client.ClientRegistry;
+import org.eclipse.leshan.server.client.ClientRegistryListener;
+import org.eclipse.leshan.server.client.ClientUpdate;
+import org.eclipse.leshan.server.demo.cluster.serialization.ClientSerDes;
+import org.eclipse.leshan.util.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPubSub;
+import redis.clients.jedis.ScanParams;
+import redis.clients.jedis.ScanResult;
+import redis.clients.util.Pool;
+
+/**
+ * A client registry storing registration in Redis.
+ * 
+ * the main key is the client end-point and the registration key is used as a secondary index
+ */
+public class RedisClientRegistry implements ClientRegistry {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RedisClientRegistry.class);
+
+    private static final String EP_CLIENT = "EP#CLIENT#";
+
+    private static final String EXPIRE = "EXPIRE#";
+
+    private static final String REG_EP = "REG#EP#";
+
+    private final Pool<Jedis> pool;
+
+    private final List<ClientRegistryListener> listeners = new CopyOnWriteArrayList<>();
+
+    public RedisClientRegistry(Pool<Jedis> p) {
+        this.pool = p;
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                final String pattern = "__keyspace@0__:" + EXPIRE + EP_CLIENT;
+                do {
+                    try (Jedis j = pool.getResource()) {
+                        // TODO not sure this is a good idea to set config here
+                        j.configSet("notify-keyspace-events", "Kx");
+                        j.psubscribe(new JedisPubSub() {
+                            @Override
+                            public void onPMessage(String pattern, String channel, String message) {
+                                String endpoint = channel.substring(pattern.length() - 1);
+                                handleRegistrationExpiration(endpoint);
+                            }
+                        }, pattern + "*");
+                    } catch (Throwable e) {
+                        LOG.warn("Redis PSUBSCRIBE interrupted.", e);
+                    }
+
+                    // wait & re-launch
+                    try {
+                        Thread.sleep(500);
+                    } catch (InterruptedException e) {
+                    }
+                    LOG.warn("Relaunch Redis PSUBSCRIBE.");
+                } while (true);
+            }
+        }).start();
+    }
+
+    @Override
+    public Client get(String endpoint) {
+        Validate.notNull(endpoint);
+        try (Jedis j = pool.getResource()) {
+            byte[] data = j.get((EP_CLIENT + endpoint).getBytes());
+            if (data == null) {
+                return null;
+            } else {
+                Client c = deserialize(data);
+                return c.isAlive() ? c : null;
+            }
+        }
+    }
+
+    @Override
+    public Collection<Client> allClients() {
+        try (Jedis j = pool.getResource()) {
+            ScanParams params = new ScanParams().match(EP_CLIENT + "*").count(100);
+            Collection<Client> list = new LinkedList<>();
+            String cursor = "0";
+            do {
+                ScanResult<byte[]> res = j.scan(cursor.getBytes(), params);
+                for (byte[] key : res.getResult()) {
+                    byte[] element = j.get(key);
+                    Client c = deserialize(element);
+                    if (c.isAlive()) {
+                        list.add(c);
+                    }
+                }
+                cursor = res.getStringCursor();
+            } while (!"0".equals(cursor));
+            return list;
+        }
+    }
+
+    @Override
+    public boolean registerClient(Client client) {
+        byte[] data = serialize(client);
+        try (Jedis j = pool.getResource()) {
+            byte[] k = (EP_CLIENT + client.getEndpoint()).getBytes();
+            byte[] old = j.getSet(k, data);
+
+            // create expire key
+            j.setex(ByteArrayUtils.concatenate(EXPIRE.getBytes(), k), client.getLifeTimeInSec().intValue(),
+                    new byte[0]);
+            j.expire(k, client.getLifeTimeInSec().intValue() + 60);
+
+            // secondary index
+            byte[] idx = (REG_EP + client.getRegistrationId()).getBytes();
+            j.set(idx, client.getEndpoint().getBytes());
+
+            j.expire(idx, client.getLifeTimeInSec().intValue());
+
+            if (old != null) {
+                Client oldClient = deserialize(old);
+                for (ClientRegistryListener l : listeners) {
+                    l.unregistered(oldClient);
+                }
+            }
+            for (ClientRegistryListener l : listeners) {
+                l.registered(client);
+            }
+            return true;
+        }
+    }
+
+    @Override
+    public Client updateClient(ClientUpdate update) {
+        try (Jedis j = pool.getResource()) {
+            // fetch the client ep by registration ID index
+            byte[] key = j.get((REG_EP + update.getRegistrationId()).getBytes());
+            if (key == null) {
+                return null;
+            }
+
+            // fetch the client
+            byte[] data = j.get(ByteArrayUtils.concatenate(EP_CLIENT.getBytes(), key));
+            if (data == null) {
+                return null;
+            }
+
+            Client c = deserialize(data);
+            Client clientUpdated = update.updateClient(c);
+            // store the new client
+            byte[] k = (EP_CLIENT + clientUpdated.getEndpoint()).getBytes();
+
+            // update expiratation
+            j.setex(ByteArrayUtils.concatenate(EXPIRE.getBytes(), k), c.getLifeTimeInSec().intValue(), new byte[0]);
+            j.setex(k, clientUpdated.getLifeTimeInSec().intValue() + 60, serialize(clientUpdated));
+
+            byte[] idx = (REG_EP + clientUpdated.getRegistrationId()).getBytes();
+
+            j.expire(idx, clientUpdated.getLifeTimeInSec().intValue());
+
+            // notify listener
+            for (ClientRegistryListener l : listeners) {
+                l.updated(update, clientUpdated);
+            }
+            return clientUpdated;
+        }
+    }
+
+    @Override
+    public Client deregisterClient(String registrationId) {
+        try (Jedis j = pool.getResource()) {
+            byte[] regKey = (REG_EP + registrationId).getBytes();
+            byte[] delRegKey = ("TODELETE#" + REG_EP + registrationId).getBytes();
+
+            // first rename for atomicity
+            if (!"OK".equals(j.rename(regKey, delRegKey))) {
+                return null;
+            }
+
+            // fetch the client ep by registration ID index
+            byte[] key = j.get(delRegKey);
+            if (key == null) {
+                return null;
+            }
+
+            byte[] epKey = ByteArrayUtils.concatenate(EP_CLIENT.getBytes(), key);
+            byte[] data = j.get(epKey);
+
+            Client c = deserialize(data);
+
+            // delete everything
+            j.del(delRegKey);
+            j.del(epKey);
+            j.del((EXPIRE + key).getBytes());
+
+            for (ClientRegistryListener l : listeners) {
+                l.unregistered(c);
+            }
+            return c;
+        }
+    }
+
+    @Override
+    public Client findByRegistrationId(String registrationId) {
+        try (Jedis j = pool.getResource()) {
+            byte[] key = j.get((REG_EP + registrationId).getBytes());
+            if (key == null) {
+                return null;
+            }
+            byte[] data = j.get(ByteArrayUtils.concatenate(EP_CLIENT.getBytes(), key));
+
+            return deserialize(data);
+        }
+    }
+
+    private void handleRegistrationExpiration(final String endpoint) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try (Jedis j = pool.getResource()) {
+                    // rename to be able to do GET/DEL in an atomic way
+                    String key = EP_CLIENT + endpoint;
+                    byte[] atomicKey = ("TODELETE#" + key).getBytes();
+                    if (!"OK".equals(j.rename(key.getBytes(), atomicKey)))
+                        return;
+
+                    // get the client
+                    byte[] val = j.get(atomicKey);
+                    Client c = deserialize(val);
+
+                    // remove client
+                    j.del(atomicKey);
+
+                    // raise event
+                    for (ClientRegistryListener listener : listeners) {
+                        listener.unregistered(c);
+                    }
+                } catch (Throwable e) {
+                    LOG.error("Unexpected exception pending registration expiration.", e);
+                }
+            }
+        }).start();
+    }
+
+    @Override
+    public void addListener(ClientRegistryListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(ClientRegistryListener listener) {
+        listeners.remove(listener);
+    }
+
+    private byte[] serialize(Client client) {
+        return ClientSerDes.serialize(client);
+    }
+
+    private Client deserialize(byte[] data) {
+        return ClientSerDes.deserialize(data);
+    }
+
+}
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java
new file mode 100644
index 00000000..9888c88d
--- /dev/null
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java
@@ -0,0 +1,357 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.californium.core.network.serialization.DataParser;
+import org.eclipse.californium.core.network.serialization.DataSerializer;
+import org.eclipse.californium.core.observe.NotificationListener;
+import org.eclipse.californium.core.observe.ObservationStore;
+import org.eclipse.californium.elements.CorrelationContext;
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.codec.InvalidValueException;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.server.californium.CaliforniumObservationRegistry;
+import org.eclipse.leshan.server.client.Client;
+import org.eclipse.leshan.server.client.ClientRegistry;
+import org.eclipse.leshan.server.model.LwM2mModelProvider;
+import org.eclipse.leshan.server.observation.ObservationRegistry;
+import org.eclipse.leshan.server.observation.ObservationRegistryListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import redis.clients.jedis.Jedis;
+import redis.clients.util.Pool;
+
+/**
+ * A registry storing observations in a Redis store.
+ * 
+ * Observations are stored using the token as primary key and a secondary index based on the registration Id.
+ */
+public class RedisObservationRegistry
+        implements CaliforniumObservationRegistry, ObservationRegistry, NotificationListener {
+
+    private final Logger LOG = LoggerFactory.getLogger(RedisObservationRegistry.class);
+
+    private final ObservationStore observationStore;
+    private final ClientRegistry clientRegistry;
+    private final LwM2mModelProvider modelProvider;
+    private Endpoint nonSecureEndpoint;
+    private Endpoint secureEndpoint;
+
+    private final List<ObservationRegistryListener> listeners = new CopyOnWriteArrayList<>();
+
+    private final Pool<Jedis> pool;
+
+    // Redis key prefixes
+    private static final byte[] OBS_TKN = "OBS#TKN#".getBytes();
+    private static final String OBS_REG = "OBS#REG#";
+
+    // observation field names (ser/des)
+    private static final byte[] ID = "id".getBytes();
+    private static final byte[] PATH = "path".getBytes();
+    private static final byte[] REGID = "regid".getBytes();
+
+    public RedisObservationRegistry(Pool<Jedis> pool, ClientRegistry clientRegistry, LwM2mModelProvider modelProvider) {
+        this.pool = pool;
+        this.modelProvider = modelProvider;
+        this.clientRegistry = clientRegistry;
+        this.observationStore = new RedisObservationStore(pool);
+    }
+
+    @Override
+    public void addObservation(Observation observation) {
+        try (Jedis j = pool.getResource()) {
+
+            j.hmset(toTokenKey(observation.getId()), serialize(observation));
+            j.sadd(toRegIdKey(observation.getRegistrationId()), observation.getId()); // secondary index
+
+            for (ObservationRegistryListener listener : listeners) {
+                listener.newObservation(observation);
+            }
+        }
+    }
+
+    @Override
+    public void setNonSecureEndpoint(Endpoint endpoint) {
+        nonSecureEndpoint = endpoint;
+    }
+
+    @Override
+    public void setSecureEndpoint(Endpoint endpoint) {
+        secureEndpoint = endpoint;
+    }
+
+    @Override
+    public int cancelObservations(Client client) {
+        // check registration id
+        String registrationId = client.getRegistrationId();
+        if (registrationId == null)
+            return 0;
+
+        Set<Observation> observations = getObservations(registrationId);
+        for (Observation observation : observations) {
+            cancelObservation(observation);
+        }
+        return observations.size();
+    }
+
+    @Override
+    public int cancelObservations(Client client, String resourcepath) {
+        // check registration id
+        String registrationId = client.getRegistrationId();
+        if (registrationId == null || resourcepath == null || resourcepath.isEmpty())
+            return 0;
+
+        Set<Observation> observations = getObservations(registrationId, resourcepath);
+        for (Observation observation : observations) {
+            cancelObservation(observation);
+        }
+        return observations.size();
+    }
+
+    @Override
+    public void cancelObservation(Observation observation) {
+        if (observation == null)
+            return;
+
+        if (secureEndpoint != null)
+            secureEndpoint.cancelObservation(observation.getId());
+        if (nonSecureEndpoint != null)
+            nonSecureEndpoint.cancelObservation(observation.getId());
+
+        try (Jedis j = pool.getResource()) {
+            j.del(toTokenKey(observation.getId()));
+
+            byte[] regIdKey = toRegIdKey(observation.getRegistrationId());
+            j.srem(regIdKey, observation.getId());
+            if (j.scard(regIdKey) == 0) {
+                // remove regId entry if no more observations
+                j.del(regIdKey);
+            }
+        }
+
+        for (ObservationRegistryListener listener : listeners) {
+            listener.cancelled(observation);
+        }
+    }
+
+    @Override
+    public Set<Observation> getObservations(Client client) {
+        return getObservations(client.getRegistrationId());
+    }
+
+    private Set<Observation> getObservations(String registrationId) {
+        if (registrationId == null)
+            return Collections.emptySet();
+
+        Set<Observation> result = new HashSet<Observation>();
+
+        try (Jedis j = pool.getResource()) {
+            Set<byte[]> tokens = j.smembers(toRegIdKey(registrationId));
+            if (tokens != null && !tokens.isEmpty()) {
+                for (byte[] token : tokens) {
+                    Observation obs = deserialize(j.hgetAll(toTokenKey(token)));
+                    if (obs != null) {
+                        result.add(obs);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    private Set<Observation> getObservations(String registrationId, String resourcePath) {
+        if (registrationId == null || resourcePath == null)
+            return Collections.emptySet();
+
+        Set<Observation> result = new HashSet<Observation>();
+        LwM2mPath lwPath = new LwM2mPath(resourcePath);
+        for (Observation obs : this.getObservations(registrationId)) {
+            if (lwPath.equals(obs.getPath())) {
+                result.add(obs);
+            }
+        }
+        return result;
+    }
+
+    private Observation getObservation(byte[] token) {
+        if (token == null || token.length == 0)
+            return null;
+
+        try (Jedis j = pool.getResource()) {
+            return deserialize(j.hgetAll(toTokenKey(token)));
+        }
+    }
+
+    public ObservationStore getObservationStore() {
+        return observationStore;
+    }
+
+    @Override
+    public void addListener(ObservationRegistryListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(ObservationRegistryListener listener) {
+        listeners.remove(listener);
+    }
+
+    private byte[] toTokenKey(byte[] token) {
+        byte[] key = new byte[OBS_TKN.length + token.length];
+        System.arraycopy(OBS_TKN, 0, key, 0, OBS_TKN.length);
+        System.arraycopy(token, 0, key, OBS_TKN.length, token.length);
+        return key;
+    }
+
+    private byte[] toRegIdKey(String registrationID) {
+        return (OBS_REG + registrationID).getBytes();
+    }
+
+    // ********* Serialization/De-serialization **********
+
+    private Map<byte[], byte[]> serialize(Observation observation) {
+        Map<byte[], byte[]> fields = new HashMap<>();
+        fields.put(ID, observation.getId());
+        fields.put(PATH, observation.getPath().toString().getBytes());
+        fields.put(REGID, observation.getRegistrationId().getBytes());
+        return fields;
+    }
+
+    private Observation deserialize(Map<byte[], byte[]> fields) {
+        if (fields == null) {
+            return null;
+        }
+        byte[] id = fields.get(ID);
+        if (id == null) {
+            return null;
+        }
+        byte[] path = fields.get(PATH);
+        if (path == null) {
+            return null;
+        }
+        byte[] regId = fields.get(REGID);
+        if (regId == null) {
+            return null;
+        }
+        return new Observation(id, new String(regId), new LwM2mPath(new String(path)));
+    }
+
+    // ********** NotificationListener interface **********
+
+    @Override
+    public void onNotification(Request coapRequest, Response coapResponse) {
+        if (listeners.isEmpty())
+            return;
+
+        if (coapResponse.getCode() == CoAP.ResponseCode.CHANGED
+                || coapResponse.getCode() == CoAP.ResponseCode.CONTENT) {
+            try {
+                // get observation for this request
+                Observation observation = this.getObservation(coapResponse.getToken());
+                if (observation == null)
+                    return;
+
+                // get client for this registration ID
+                Client client = clientRegistry.findByRegistrationId(observation.getRegistrationId());
+                if (client == null)
+                    // TODO Should we clean registrationIDs maps ?
+                    return;
+
+                // get model for this client
+                LwM2mModel model = modelProvider.getObjectModel(client);
+
+                // decode response
+                LwM2mNode content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
+                        ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), observation.getPath(),
+                        model);
+
+                // notify all listeners
+                for (ObservationRegistryListener listener : listeners) {
+                    listener.newValue(observation, content);
+                }
+            } catch (InvalidValueException e) {
+                String msg = String.format("[%s] ([%s])", e.getMessage(), e.getPath().toString());
+                LOG.debug(msg);
+            }
+        }
+    }
+
+    /**
+     * An {@link ObservationStore} storing requests in a Redis store.
+     * 
+     * The CoAP request is serialized using the Californium network serialization (see {@link DataParser} and
+     * {@link DataSerializer})
+     */
+    private static class RedisObservationStore implements ObservationStore {
+
+        private final Pool<Jedis> pool;
+
+        public RedisObservationStore(Pool<Jedis> pool) {
+            this.pool = pool;
+        }
+
+        @Override
+        public void add(org.eclipse.californium.core.observe.Observation obs) {
+            try (Jedis j = pool.getResource()) {
+                j.set(obs.getRequest().getToken(), DataSerializer.serializeRequest(obs.getRequest()));
+            }
+        }
+
+        @Override
+        public void remove(byte[] token) {
+            try (Jedis j = pool.getResource()) {
+                j.del(token);
+            }
+        }
+
+        @Override
+        public org.eclipse.californium.core.observe.Observation get(byte[] token) {
+            try (Jedis j = pool.getResource()) {
+                byte[] req = j.get(token);
+                if (req == null) {
+                    return null;
+                } else {
+                    // TODO handle security context
+                    return new org.eclipse.californium.core.observe.Observation(new DataParser(req).parseRequest(),
+                            null);
+                }
+            }
+        }
+
+        @Override
+        public void setContext(byte[] token, CorrelationContext correlationContext) {
+            // TODO handle security context
+        }
+    }
+
+}
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisSecurityRegistry.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisSecurityRegistry.java
new file mode 100644
index 00000000..f6b9e1c6
--- /dev/null
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisSecurityRegistry.java
@@ -0,0 +1,174 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.eclipse.leshan.server.demo.cluster.serialization.SecurityInfoSerDes;
+import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
+import org.eclipse.leshan.server.security.SecurityInfo;
+import org.eclipse.leshan.server.security.SecurityRegistry;
+
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.ScanParams;
+import redis.clients.jedis.ScanResult;
+import redis.clients.util.Pool;
+
+/**
+ * A security registry storing values in a Redis store.
+ * 
+ * Only root CA (trust chain) used for validating X.509 certificate and raw public/private server keys are stored in
+ * each node memory. Because they are static.
+ * 
+ * Security info are stored using the endpoint as primary key and a secondary index is created for psk-identity lookup.
+ */
+public class RedisSecurityRegistry implements SecurityRegistry {
+
+    private static final String SEC_EP = "SEC#EP#";
+
+    private static final String PSKID_SEC = "PSKID#SEC";
+
+    private final Pool<Jedis> pool;
+
+    private PublicKey serverPublicKey = null;
+
+    private PrivateKey serverPrivateKey = null;
+
+    private X509Certificate[] serverX509CertChain = null;
+
+    private Certificate[] trustedCertificates = null;
+
+    public RedisSecurityRegistry(Pool<Jedis> pool, PrivateKey serverPrivateKey, X509Certificate[] serverX509CertChain,
+            Certificate[] trustedCertificates) {
+        this.pool = pool;
+        this.serverPrivateKey = serverPrivateKey;
+        this.serverX509CertChain = serverX509CertChain;
+        this.trustedCertificates = trustedCertificates;
+    }
+
+    public RedisSecurityRegistry(Pool<Jedis> pool, PrivateKey serverPrivateKey, PublicKey serverPublicKey) {
+        this.pool = pool;
+        this.serverPrivateKey = serverPrivateKey;
+        this.serverPublicKey = serverPublicKey;
+    }
+
+    @Override
+    public SecurityInfo getByEndpoint(String endpoint) {
+        try (Jedis j = pool.getResource()) {
+            byte[] data = j.get((SEC_EP + endpoint).getBytes());
+            if (data == null) {
+                return null;
+            } else {
+                return deserialize(data);
+            }
+        }
+    }
+
+    @Override
+    public SecurityInfo getByIdentity(String identity) {
+        try (Jedis j = pool.getResource()) {
+            String ep = j.hget(PSKID_SEC, identity);
+            if (ep == null) {
+                return null;
+            } else {
+                byte[] data = j.get((SEC_EP + ep).getBytes());
+                if (data == null) {
+                    return null;
+                } else {
+                    return deserialize(data);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Collection<SecurityInfo> getAll() {
+        try (Jedis j = pool.getResource()) {
+            ScanParams params = new ScanParams().match(SEC_EP + "*").count(100);
+            Collection<SecurityInfo> list = new LinkedList<>();
+            String cursor = "0";
+            do {
+                ScanResult<byte[]> res = j.scan(cursor.getBytes(), params);
+                for (byte[] key : res.getResult()) {
+                    byte[] element = j.get(key);
+                    list.add(deserialize(element));
+                }
+                cursor = res.getStringCursor();
+            } while (!"0".equals(cursor));
+            return list;
+        }
+    }
+
+    @Override
+    public SecurityInfo add(SecurityInfo info) throws NonUniqueSecurityInfoException {
+        byte[] data = serialize(info);
+        try (Jedis j = pool.getResource()) {
+            j.set((SEC_EP + info.getEndpoint()).getBytes(), data);
+            if (info.getIdentity() != null) {
+                // populate the secondary index (security info by PSK id)
+                j.hset(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes());
+            }
+            return null;
+        }
+    }
+
+    @Override
+    public SecurityInfo remove(String endpoint) {
+        try (Jedis j = pool.getResource()) {
+            byte[] data = j.get((SEC_EP + endpoint).getBytes());
+
+            SecurityInfo info = deserialize(data);
+            if (info.getIdentity() != null) {
+                j.hdel(PSKID_SEC.getBytes(), info.getIdentity().getBytes());
+            }
+            j.del((SEC_EP + endpoint).getBytes());
+            return info;
+        }
+    }
+
+    @Override
+    public PublicKey getServerPublicKey() {
+        return serverPublicKey;
+    }
+
+    @Override
+    public PrivateKey getServerPrivateKey() {
+        return serverPrivateKey;
+    }
+
+    @Override
+    public X509Certificate[] getServerX509CertChain() {
+        return serverX509CertChain;
+    }
+
+    @Override
+    public Certificate[] getTrustedCertificates() {
+        return trustedCertificates;
+    }
+
+    private byte[] serialize(SecurityInfo secInfo) {
+        return SecurityInfoSerDes.serialize(secInfo);
+    }
+
+    private SecurityInfo deserialize(byte[] data) {
+        return SecurityInfoSerDes.deserialize(data);
+    }
+}
\ No newline at end of file
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDes.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDes.java
new file mode 100644
index 00000000..eea384fd
--- /dev/null
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDes.java
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster.serialization;
+
+import java.net.InetSocketAddress;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.leshan.LinkObject;
+import org.eclipse.leshan.core.request.BindingMode;
+import org.eclipse.leshan.server.client.Client;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonArray;
+import com.eclipsesource.json.JsonObject;
+import com.eclipsesource.json.JsonValue;
+
+/**
+ * Functions for serialize and deserialize a Client in JSON for storage.
+ */
+public class ClientSerDes {
+
+    public static byte[] serialize(Client c) {
+        JsonObject o = Json.object();
+        o.add("regDate", c.getRegistrationDate().getTime());
+        o.add("address", c.getAddress().getHostAddress());
+        o.add("port", c.getPort());
+        o.add("regAddr", c.getRegistrationEndpointAddress().getHostString());
+        o.add("regPort", c.getRegistrationEndpointAddress().getPort());
+        o.add("lt", c.getLifeTimeInSec());
+        if (c.getSmsNumber() != null) {
+            o.add("sms", c.getSmsNumber());
+        }
+        o.add("ver", c.getLwM2mVersion());
+        o.add("bnd", c.getBindingMode().name());
+        o.add("ep", c.getEndpoint());
+        o.add("regId", c.getRegistrationId());
+
+        JsonArray links = new JsonArray();
+        for (LinkObject l : c.getObjectLinks()) {
+            JsonObject ol = Json.object();
+            ol.add("url", l.getUrl());
+            JsonObject at = Json.object();
+            for (Map.Entry<String, Object> e : l.getAttributes().entrySet()) {
+                if (e.getValue() instanceof Integer) {
+                    at.add(e.getKey(), (int) e.getValue());
+                } else {
+                    at.add(e.getKey(), e.getValue().toString());
+                }
+            }
+            ol.add("at", at);
+            links.add(ol);
+        }
+        o.add("objLink", links);
+        JsonObject addAttr = Json.object();
+        for (Map.Entry<String, String> e : c.getAdditionalRegistrationAttributes().entrySet()) {
+            addAttr.add(e.getKey(), e.getValue());
+        }
+        o.add("addAttr", addAttr);
+        o.add("root", c.getRootPath());
+        o.add("lastUp", c.getLastUpdate().getTime());
+        return o.toString().getBytes();
+    }
+
+    public static Client deserialize(byte[] data) {
+        JsonObject v = (JsonObject) Json.parse(new String(data));
+
+        Client.Builder b = new Client.Builder(v.getString("regId", null), v.getString("ep", null),
+                new InetSocketAddress(v.getString("address", null), v.getInt("port", 0)).getAddress(),
+                v.getInt("port", 0), new InetSocketAddress(v.getString("regAddr", null), v.getInt("regPort", 0)));
+        b.bindingMode(BindingMode.valueOf(v.getString("bnd", null)));
+        b.lastUpdate(new Date(v.getLong("lastUp", 0)));
+        b.lifeTimeInSec(v.getLong("lt", 0));
+        b.lwM2mVersion(v.getString("ver", "1.0"));
+        b.registrationDate(new Date(v.getLong("regDate", 0)));
+        if (v.get("sms") != null) {
+            b.smsNumber(v.getString("sms", ""));
+        }
+
+        JsonArray links = (JsonArray) v.get("objLink");
+        LinkObject[] linkObjs = new LinkObject[links.size()];
+        for (int i = 0; i < links.size(); i++) {
+            JsonObject ol = (JsonObject) links.get(i);
+
+            Map<String, Object> attMap = new HashMap<>();
+            JsonObject att = (JsonObject) ol.get("at");
+            for (String k : att.names()) {
+                JsonValue jsonValue = att.get(k);
+                if (jsonValue.isNumber()) {
+                    attMap.put(k, jsonValue.asInt());
+                } else {
+                    attMap.put(k, jsonValue.asString());
+                }
+            }
+            LinkObject o = new LinkObject(ol.getString("url", null), attMap);
+            linkObjs[i] = o;
+        }
+        b.objectLinks(linkObjs);
+        Map<String, String> addAttr = new HashMap<>();
+        JsonObject o = (JsonObject) v.get("addAttr");
+        for (String k : o.names()) {
+            addAttr.put(k, o.getString(k, ""));
+        }
+        b.additionalRegistrationAttributes(addAttr);
+
+        return b.build();
+    }
+}
\ No newline at end of file
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDes.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDes.java
new file mode 100644
index 00000000..7d15bbf8
--- /dev/null
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDes.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster.serialization;
+
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+
+import org.eclipse.leshan.server.security.SecurityInfo;
+import org.eclipse.leshan.util.Hex;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.JsonObject;
+
+/**
+ * Functions for serialize and deserialize security information in JSON for storage.
+ */
+public class SecurityInfoSerDes {
+
+    public static byte[] serialize(SecurityInfo s) {
+        JsonObject o = Json.object();
+        o.set("ep", s.getEndpoint());
+        if (s.getIdentity() != null) {
+            o.set("id", s.getIdentity());
+        }
+        if (s.getPreSharedKey() != null) {
+            o.set("psk", Hex.encodeHexString(s.getPreSharedKey()));
+        }
+        if (s.getRawPublicKey() != null) {
+            JsonObject rpk = new JsonObject();
+            ECPublicKey ecPublicKey = (ECPublicKey) s.getRawPublicKey();
+            // Get x coordinate
+            byte[] x = ecPublicKey.getW().getAffineX().toByteArray();
+            if (x[0] == 0)
+                x = Arrays.copyOfRange(x, 1, x.length);
+            rpk.add("x", Hex.encodeHexString(x));
+
+            // Get Y coordinate
+            byte[] y = ecPublicKey.getW().getAffineY().toByteArray();
+            if (y[0] == 0)
+                y = Arrays.copyOfRange(y, 1, y.length);
+            rpk.add("y", Hex.encodeHexString(y));
+
+            // Get Curves params
+            ecPublicKey.getParams();
+
+            // use only the first part as the curve name
+            rpk.add("params", ecPublicKey.getParams().toString().split(" ")[0]);
+            o.set("rpk", rpk);
+        }
+
+        if (s.useX509Cert()) {
+            o.set("x509", true);
+        }
+
+        return o.toString().getBytes();
+    }
+
+    public static SecurityInfo deserialize(byte[] data) {
+        JsonObject o = (JsonObject) Json.parse(new String(data));
+
+        SecurityInfo i = null;
+        String ep = o.getString("ep", null);
+        if (o.get("psk") != null) {
+            i = SecurityInfo.newPreSharedKeyInfo(ep, o.getString("id", null),
+                    Hex.decodeHex(o.getString("psk", null).toCharArray()));
+        } else if (o.get("x509") != null) {
+            i = SecurityInfo.newX509CertInfo(ep);
+        } else {
+            JsonObject rpk = (JsonObject) o.get("rpk");
+            PublicKey key;
+            try {
+                byte[] x = Hex.decodeHex(rpk.getString("x", null).toCharArray());
+                byte[] y = Hex.decodeHex(rpk.getString("y", null).toCharArray());
+                String params = rpk.getString("params", null);
+                AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC");
+                algoParameters.init(new ECGenParameterSpec(params));
+                ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class);
+
+                KeySpec keySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(x), new BigInteger(y)), parameterSpec);
+
+                key = KeyFactory.getInstance("EC").generatePublic(keySpec);
+            } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException
+                    | InvalidParameterSpecException e) {
+                throw new IllegalStateException("Invalid security info content", e);
+            }
+            i = SecurityInfo.newRawPublicKeyInfo(ep, key);
+        }
+        return i;
+    }
+
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/client/Client.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/client/Client.java
index 42ef8085..5d66ccf1 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/client/Client.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/client/Client.java
@@ -15,6 +15,7 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.client;
 
+import java.io.Serializable;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
@@ -31,7 +32,9 @@ import org.eclipse.leshan.util.Validate;
 /**
  * An immutable structure which represent a LW-M2M client registered on the server
  */
-public class Client {
+public class Client implements Serializable {
+
+    private static final long serialVersionUID = 1L;
 
     private static final long DEFAULT_LIFETIME_IN_SEC = 86400L;
 
@@ -109,9 +112,9 @@ public class Client {
         this.bindingMode = bindingMode == null ? BindingMode.U : bindingMode;
         this.registrationDate = registrationDate == null ? new Date() : registrationDate;
         this.lastUpdate = lastUpdate == null ? new Date() : lastUpdate;
-        this.additionalRegistrationAttributes = additionalRegistrationAttributes == null ? Collections
-                .unmodifiableMap(new HashMap<String, String>()) : Collections
-                .unmodifiableMap(additionalRegistrationAttributes);
+        this.additionalRegistrationAttributes = additionalRegistrationAttributes == null
+                ? Collections.unmodifiableMap(new HashMap<String, String>())
+                : Collections.unmodifiableMap(additionalRegistrationAttributes);
 
     }
 
@@ -172,7 +175,7 @@ public class Client {
 
         Arrays.sort(res, new Comparator<LinkObject>() {
 
-            /* sort by objectid, object instance and ressource */
+            /* sort by path */
             @Override
             public int compare(LinkObject o1, LinkObject o2) {
                 if (o1 == null && o2 == null)
@@ -181,59 +184,43 @@ public class Client {
                     return -1;
                 if (o2 == null)
                     return 1;
-                // by object
-                Integer oi1 = o1.getObjectId();
-                Integer oi2 = o2.getObjectId();
-
-                if (oi1 == null && oi2 == null) {
-                    return 0;
-                }
-                if (oi1 == null) {
-                    return -1;
-                }
-                if (oi2 == null) {
-                    return 1;
-                }
-                int oicomp = oi1.compareTo(oi2);
-                if (oicomp != 0) {
-                    return oicomp;
+                // by URL
+                String[] url1 = o1.getUrl().split("/");
+                String[] url2 = o2.getUrl().split("/");
+
+                for (int i = 0; i < url1.length && i < url2.length; i++) {
+                    // is it two numbers?
+                    if (isNumber(url1[i]) && isNumber(url2[i])) {
+                        int cmp = Integer.parseInt(url1[i]) - Integer.parseInt(url2[i]);
+                        if (cmp != 0) {
+                            return cmp;
+                        }
+                    } else {
+
+                        int v = url1[i].compareTo(url2[i]);
+
+                        if (v != 0) {
+                            return v;
+                        }
+                    }
                 }
 
-                Integer oii1 = o1.getObjectInstanceId();
-                Integer oii2 = o2.getObjectInstanceId();
-                if (oii1 == null && oii2 == null) {
-                    return 0;
-                }
-                if (oii1 == null) {
-                    return -1;
-                }
-                if (oii2 == null) {
-                    return 1;
-                }
-                int oiicomp = oii1.compareTo(oii2);
-                if (oiicomp != 0) {
-                    return oiicomp;
-                }
-
-                Integer or1 = o1.getResourceId();
-                Integer or2 = o2.getResourceId();
-                if (or1 == null && or2 == null) {
-                    return 0;
-                }
-                if (or1 == null) {
-                    return -1;
-                }
-                if (or2 == null) {
-                    return 1;
-                }
-                return or1.compareTo(or2);
-
+                return url1.length - url2.length;
             }
         });
 
         return res;
     }
 
+    private static boolean isNumber(String s) {
+        try {
+            Integer.parseInt(s);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
     public synchronized Long getLifeTimeInSec() {
         return lifeTimeInSec;
     }
@@ -280,10 +267,10 @@ public class Client {
 
     @Override
     public String toString() {
-        return String
-                .format("Client [registrationDate=%s, address=%s, port=%s, registrationEndpoint=%s, lifeTimeInSec=%s, smsNumber=%s, lwM2mVersion=%s, bindingMode=%s, endpoint=%s, registrationId=%s, objectLinks=%s, lastUpdate=%s]",
-                        registrationDate, address, port, registrationEndpointAddress, lifeTimeInSec, smsNumber,
-                        lwM2mVersion, bindingMode, endpoint, registrationId, Arrays.toString(objectLinks), lastUpdate);
+        return String.format(
+                "Client [registrationDate=%s, address=%s, port=%s, registrationEndpoint=%s, lifeTimeInSec=%s, smsNumber=%s, lwM2mVersion=%s, bindingMode=%s, endpoint=%s, registrationId=%s, objectLinks=%s, lastUpdate=%s]",
+                registrationDate, address, port, registrationEndpointAddress, lifeTimeInSec, smsNumber, lwM2mVersion,
+                bindingMode, endpoint, registrationId, Arrays.toString(objectLinks), lastUpdate);
     }
 
     /**
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/ObservationRegistryImpl.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/ObservationRegistryImpl.java
deleted file mode 100644
index b866c303..00000000
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/impl/ObservationRegistryImpl.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
- * 
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Sierra Wireless - initial API and implementation
- *******************************************************************************/
-package org.eclipse.leshan.server.impl;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.eclipse.leshan.core.node.LwM2mNode;
-import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.observation.ObservationListener;
-import org.eclipse.leshan.server.client.Client;
-import org.eclipse.leshan.server.observation.ObservationRegistry;
-import org.eclipse.leshan.server.observation.ObservationRegistryListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A <code>Map</code> based registry for keeping track of this server's observed resources on LWM2M Clients.
- * 
- */
-public class ObservationRegistryImpl implements ObservationRegistry, ObservationListener {
-
-    private final Logger LOG = LoggerFactory.getLogger(ObservationRegistryImpl.class);
-    private final Map<String /* registration id */, Map<LwM2mPath /* resource path */, Observation>> observationsByClientAndResource;
-
-    private final List<ObservationRegistryListener> listeners = new CopyOnWriteArrayList<>();
-
-    public ObservationRegistryImpl() {
-        observationsByClientAndResource = new ConcurrentHashMap<String, Map<LwM2mPath, Observation>>();
-    }
-
-    @Override
-    public synchronized void addObservation(Observation observation) {
-
-        if (observation != null) {
-            String registrationID = observation.getRegistrationId();
-
-            Map<LwM2mPath, Observation> clientObservations = observationsByClientAndResource.get(registrationID);
-            if (clientObservations == null) {
-                clientObservations = new ConcurrentHashMap<LwM2mPath, Observation>();
-                observationsByClientAndResource.put(registrationID, clientObservations);
-            }
-
-            Observation oldObservation = clientObservations.get(observation.getPath());
-            if (oldObservation != null) {
-                oldObservation.cancel();
-            }
-            clientObservations.put(observation.getPath(), observation);
-            for (ObservationRegistryListener listener : listeners) {
-                listener.newObservation(observation);
-            }
-            observation.addListener(this);
-        }
-    }
-
-    @Override
-    public synchronized int cancelObservations(Client client) {
-        int count = 0;
-        if (client != null) {
-            Map<LwM2mPath, Observation> clientObservations = observationsByClientAndResource.get(client
-                    .getRegistrationId());
-
-            if (clientObservations != null) {
-                count = clientObservations.size();
-                if (LOG.isTraceEnabled()) {
-                    LOG.trace("Canceling {} observations of client {}", count, client.getEndpoint());
-                }
-                for (Observation obs : clientObservations.values()) {
-                    obs.cancel();
-                }
-                clientObservations.clear();
-                observationsByClientAndResource.remove(client.getRegistrationId());
-            }
-        }
-        return count;
-    }
-
-    @Override
-    public synchronized void cancelObservation(Client client, String resourcepath) {
-        if (client != null && resourcepath != null) {
-            Map<LwM2mPath, Observation> clientObservations = observationsByClientAndResource.get(client
-                    .getRegistrationId());
-
-            if (clientObservations != null) {
-                LwM2mPath lwM2mResourcePath = new LwM2mPath(resourcepath);
-                Observation observation = clientObservations.get(lwM2mResourcePath);
-                if (observation != null) {
-                    // observationsByClientAndResource will be cleaned in ObservationRegistryImpl.cancelled()
-                    observation.cancel();
-                }
-            }
-        }
-    }
-
-    @Override
-    public Set<Observation> getObservations(Client client) {
-        Map<LwM2mPath, Observation> observations = observationsByClientAndResource.get(client.getRegistrationId());
-        if (observations == null)
-            return Collections.emptySet();
-        else
-            return Collections.unmodifiableSet(new HashSet<Observation>(observations.values()));
-    }
-
-    @Override
-    public void addListener(ObservationRegistryListener listener) {
-        listeners.add(listener);
-    }
-
-    @Override
-    public void removeListener(ObservationRegistryListener listener) {
-        listeners.remove(listener);
-    }
-
-    @Override
-    public void cancelled(Observation observation) {
-        // fire cancelled event
-        for (ObservationRegistryListener listener : listeners) {
-            listener.cancelled(observation);
-        }
-
-        synchronized (this) {
-            // clear the observationsByClientAndResource map
-            Map<LwM2mPath, Observation> observations = observationsByClientAndResource.get(observation
-                    .getRegistrationId());
-            if (observations != null) {
-                LwM2mPath path = observation.getPath();
-                if (LOG.isTraceEnabled()) {
-                    LOG.trace("Canceling {} observation of registration {}", path, observation.getRegistrationId());
-                }
-                observations.remove(path);
-                if (observations.isEmpty()) {
-                    observationsByClientAndResource.remove(observation.getRegistrationId());
-                }
-            }
-        }
-    }
-
-    @Override
-    public void newValue(Observation observation, LwM2mNode value) {
-        for (ObservationRegistryListener listener : listeners) {
-            listener.newValue(observation, value);
-        }
-    }
-}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistry.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistry.java
index 49971262..20f60231 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistry.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistry.java
@@ -45,15 +45,26 @@ public interface ObservationRegistry {
     int cancelObservations(Client client);
 
     /**
-     * Cancels the active observations for the given resource.
+     * Cancels all active observations for the given resource of a given client.
      * 
      * As a consequence the LWM2M Client will stop sending notifications about updated values of resources in scope of
      * the canceled observation.
      * 
      * @param client the LWM2M Client to cancel observation for
      * @param resourcepath resource to cancel observation for
+     * @return the number of canceled observations
+     */
+    int cancelObservations(Client client, String resourcepath);
+
+    /**
+     * Cancels an observation.
+     * 
+     * As a consequence the LWM2M Client will stop sending notifications about updated values of resources in scope of
+     * the canceled observation.
+     * 
+     * @param observation the observation to cancel.
      */
-    void cancelObservation(Client client, String resourcepath);
+    void cancelObservation(Observation observation);
 
     /**
      * Get all running observation for a given client
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
index e217a896..0ab43850 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
@@ -15,9 +15,13 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.observation;
 
+import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.observation.ObservationListener;
 
-public interface ObservationRegistryListener extends ObservationListener {
+public interface ObservationRegistryListener {
     void newObservation(Observation observation);
+
+    void cancelled(Observation observation);
+
+    void newValue(Observation observation, LwM2mNode value);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/security/SecurityInfo.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/security/SecurityInfo.java
index a8869d1a..ed374df7 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/security/SecurityInfo.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/security/SecurityInfo.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.server.security;
 
 import java.io.Serializable;
 import java.security.PublicKey;
+import java.util.Arrays;
 
 import org.eclipse.leshan.util.Validate;
 
@@ -83,6 +84,9 @@ public class SecurityInfo implements Serializable {
         return endpoint;
     }
 
+    /**
+     * The Pre-Shared-Key identity
+     */
     public String getIdentity() {
         return identity;
     }
@@ -98,4 +102,48 @@ public class SecurityInfo implements Serializable {
     public boolean useX509Cert() {
         return useX509Cert;
     }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((endpoint == null) ? 0 : endpoint.hashCode());
+        result = prime * result + ((identity == null) ? 0 : identity.hashCode());
+        result = prime * result + Arrays.hashCode(preSharedKey);
+        result = prime * result + ((rawPublicKey == null) ? 0 : rawPublicKey.hashCode());
+        result = prime * result + (useX509Cert ? 1231 : 1237);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        SecurityInfo other = (SecurityInfo) obj;
+        if (endpoint == null) {
+            if (other.endpoint != null)
+                return false;
+        } else if (!endpoint.equals(other.endpoint))
+            return false;
+        if (identity == null) {
+            if (other.identity != null)
+                return false;
+        } else if (!identity.equals(other.identity))
+            return false;
+        if (!Arrays.equals(preSharedKey, other.preSharedKey))
+            return false;
+        if (rawPublicKey == null) {
+            if (other.rawPublicKey != null)
+                return false;
+        } else if (!rawPublicKey.equals(other.rawPublicKey))
+            return false;
+        if (useX509Cert != other.useX509Cert)
+            return false;
+        return true;
+    }
+
 }
diff --git a/leshan-server-demo/pom.xml b/leshan-server-demo/pom.xml
index c7913d1b..4546f6d5 100644
--- a/leshan-server-demo/pom.xml
+++ b/leshan-server-demo/pom.xml
@@ -31,6 +31,10 @@ Contributors:
             <groupId>org.eclipse.leshan</groupId>
             <artifactId>leshan-server-cf</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.eclipse.leshan</groupId>
+            <artifactId>leshan-server-cluster</artifactId>
+        </dependency>
         <dependency>
             <groupId>commons-cli</groupId>
             <artifactId>commons-cli</artifactId>
@@ -55,6 +59,7 @@ Contributors:
             <groupId>org.eclipse.jetty</groupId>
             <artifactId>jetty-continuation</artifactId>
         </dependency>
+
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
@@ -71,6 +76,13 @@ Contributors:
             <artifactId>log4j-slf4j-impl</artifactId>
             <scope>runtime</scope>
         </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
index 565d6087..dc4a933b 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
@@ -12,11 +12,13 @@
  * 
  * Contributors:
  *     Sierra Wireless - initial API and implementation
+ *     Bosch Software Innovations - added Redis URL support with authentication
  *******************************************************************************/
 package org.eclipse.leshan.server.demo;
 
 import java.math.BigInteger;
 import java.net.BindException;
+import java.net.URI;
 import java.security.AlgorithmParameters;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
@@ -41,15 +43,25 @@ import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.webapp.WebAppContext;
 import org.eclipse.leshan.server.californium.LeshanServerBuilder;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
+import org.eclipse.leshan.server.client.ClientRegistry;
+import org.eclipse.leshan.server.demo.cluster.RedisClientRegistry;
+import org.eclipse.leshan.server.demo.cluster.RedisObservationRegistry;
+import org.eclipse.leshan.server.demo.cluster.RedisSecurityRegistry;
 import org.eclipse.leshan.server.demo.servlet.ClientServlet;
 import org.eclipse.leshan.server.demo.servlet.EventServlet;
 import org.eclipse.leshan.server.demo.servlet.ObjectSpecServlet;
 import org.eclipse.leshan.server.demo.servlet.SecurityServlet;
 import org.eclipse.leshan.server.impl.SecurityRegistryImpl;
+import org.eclipse.leshan.server.model.LwM2mModelProvider;
+import org.eclipse.leshan.server.model.StandardModelProvider;
 import org.eclipse.leshan.util.Hex;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.util.Pool;
+
 public class LeshanServerDemo {
 
     private static final Logger LOG = LoggerFactory.getLogger(LeshanServerDemo.class);
@@ -67,6 +79,8 @@ public class LeshanServerDemo {
         options.addOption("slh", "coapshost", true, "Set the secure local CoAP address.\nDefault: any local address.");
         options.addOption("slp", "coapsport", true, "Set the secure local CoAP port.\nDefault: 5684.");
         options.addOption("wp", "webport", true, "Set the HTTP port for web server.\nDefault: 8080.");
+        options.addOption("r", "redis", true,
+                "Set the location of the Redis database for running in cluster mode. The URL is in the format of: 'redis://:password@hostname:port/db_number'\nExample without DB and password: 'redis://localhost:6379'\nDefault: none, no Redis connection.");
         HelpFormatter formatter = new HelpFormatter();
         formatter.setOptionComparator(null);
 
@@ -131,8 +145,14 @@ public class LeshanServerDemo {
             webPort = Integer.parseInt(webPortOption);
         }
 
+        // get the Redis hostname:port
+        String redisUrl = null;
+        if (cl.hasOption("r")) {
+            redisUrl = cl.getOptionValue("r");
+        }
+
         try {
-            createAndStartServer(webPort, localAddress, localPort, secureLocalAddress, secureLocalPort);
+            createAndStartServer(webPort, localAddress, localPort, secureLocalAddress, secureLocalPort, redisUrl);
         } catch (BindException e) {
             System.out.println(String.format("Web port %s is alreay used, you could change it using 'webport' option.",
                     webPort));
@@ -143,12 +163,19 @@ public class LeshanServerDemo {
     }
 
     public static void createAndStartServer(int webPort, String localAddress, int localPort, String secureLocalAddress,
-            int secureLocalPort) throws Exception {
+            int secureLocalPort, String redisUrl) throws Exception {
         // Prepare LWM2M server
         LeshanServerBuilder builder = new LeshanServerBuilder();
         builder.setLocalAddress(localAddress, localPort);
         builder.setLocalSecureAddress(secureLocalAddress, secureLocalPort);
 
+        // connect to redis if needed
+        Pool<Jedis> jedis = null;
+        if (redisUrl != null) {
+            // TODO: support sentinel pool and make pool configurable
+            jedis = new JedisPool(new URI(redisUrl));
+        }
+
         // Get public and private server key
         PrivateKey privateKey = null;
         PublicKey publicKey = null;
@@ -175,9 +202,22 @@ public class LeshanServerDemo {
             publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
             privateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec);
 
-            builder.setSecurityRegistry(new SecurityRegistryImpl(privateKey, publicKey));
+            LwM2mModelProvider modelProvider = new StandardModelProvider();
+            builder.setObjectModelProvider(modelProvider);
+
+            if (jedis == null) {
+                // in memory security registry (with file persistence)
+                builder.setSecurityRegistry(new SecurityRegistryImpl(privateKey, publicKey));
+            } else {
+                // use Redis
+                ClientRegistry clientRegistry = new RedisClientRegistry(jedis);
+                builder.setSecurityRegistry(new RedisSecurityRegistry(jedis, privateKey, publicKey));
+                builder.setClientRegistry(clientRegistry);
+                builder.setObservationRegistry(new RedisObservationRegistry(jedis, clientRegistry, modelProvider));
+            }
         } catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidParameterSpecException e) {
-            LOG.warn("Unable to load RPK.", e);
+            LOG.error("Unable to initialize RPK.", e);
+            System.exit(-1);
         }
 
         // Create and start LWM2M server
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
index 931ea30f..29c47131 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
@@ -329,7 +329,7 @@ public class ClientServlet extends HttpServlet {
                 String target = StringUtils.substringsBetween(req.getPathInfo(), clientEndpoint, "/observe")[0];
                 Client client = server.getClientRegistry().get(clientEndpoint);
                 if (client != null) {
-                    server.getObservationRegistry().cancelObservation(client, target);
+                    server.getObservationRegistry().cancelObservations(client, target);
                     resp.setStatus(HttpServletResponse.SC_OK);
                 } else {
                     resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
diff --git a/pom.xml b/pom.xml
index 280b4fd9..09c04bde 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,7 @@ Contributors:
         <module>leshan-core</module>
         <module>leshan-server-core</module>
         <module>leshan-server-cf</module>
+        <module>leshan-server-cluster</module>
         <module>leshan-client-core</module>
         <module>leshan-client-cf</module>
 
@@ -103,7 +104,7 @@ Contributors:
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <californium.version>1.0.3</californium.version>
+        <californium.version>2.0.0-M1</californium.version>
         <log4j.version>2.1</log4j.version>
         <slf4j.api.version>1.7.10</slf4j.api.version>
         <jetty.version>9.1.4.v20140401</jetty.version>
@@ -208,50 +209,50 @@ Contributors:
 
         <pluginManagement>
             <plugins>
-	            <plugin>
-	                <groupId>org.apache.maven.plugins</groupId>
-	                <artifactId>maven-compiler-plugin</artifactId>
-	                <version>3.3</version>
-	                <configuration>
-	                    <encoding>UTF-8</encoding>
-	                    <source>1.7</source>
-	                    <target>1.7</target>
-	                    <debug>true</debug>
-	                    <optimize>true</optimize>
-	                    <showDeprecations>true</showDeprecations>
-	                </configuration>
-	            </plugin>
-	            <plugin>
-	                <groupId>org.apache.maven.plugins</groupId>
-	                <artifactId>maven-surefire-plugin</artifactId>
-	                <version>2.19</version>
-	                <configuration>
-	                    <parallel>classes</parallel>
-	                    <threadCount>4</threadCount>
-	                    <excludes>
-	                        <exclude>**/*$*</exclude>
-	                    </excludes>
-	                </configuration>
-	            </plugin>
-	            <plugin>
-	                <artifactId>maven-jar-plugin</artifactId>
-	                <version>2.6</version>
-	                <configuration>
-	                    <archive>
-	                        <manifestEntries>
-	                            <Implementation-Version>${project.version}</Implementation-Version>
-	                        </manifestEntries>
-	                    </archive>
-	                </configuration>
-	            </plugin>
-	            <plugin>
-	                <groupId>org.apache.maven.plugins</groupId>
-	                <artifactId>maven-javadoc-plugin</artifactId>
-	                <version>2.10.3</version>
-	                <configuration>
-	                    <excludePackageNames>org.eclipse.leshan.util</excludePackageNames>
-	                </configuration>
-	            </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.3</version>
+                    <configuration>
+                        <encoding>UTF-8</encoding>
+                        <source>1.7</source>
+                        <target>1.7</target>
+                        <debug>true</debug>
+                        <optimize>true</optimize>
+                        <showDeprecations>true</showDeprecations>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>2.19</version>
+                    <configuration>
+                        <parallel>classes</parallel>
+                        <threadCount>4</threadCount>
+                        <excludes>
+                            <exclude>**/*$*</exclude>
+                        </excludes>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-jar-plugin</artifactId>
+                    <version>2.6</version>
+                    <configuration>
+                        <archive>
+                            <manifestEntries>
+                                <Implementation-Version>${project.version}</Implementation-Version>
+                            </manifestEntries>
+                        </archive>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-javadoc-plugin</artifactId>
+                    <version>2.10.3</version>
+                    <configuration>
+                        <excludePackageNames>org.eclipse.leshan.util</excludePackageNames>
+                    </configuration>
+                </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-eclipse-plugin</artifactId>
@@ -283,21 +284,21 @@ Contributors:
                     <artifactId>maven-bundle-plugin</artifactId>
                     <version>3.0.1</version>
                 </plugin>
-	            <plugin>
-	                <groupId>org.codehaus.mojo</groupId>
-	                <artifactId>buildnumber-maven-plugin</artifactId>
-	                <version>1.4</version>
-	                <executions>
-	                    <execution>
-	                        <goals>
-	                            <goal>create</goal>
-	                        </goals>
-	                    </execution>
-	                </executions>
-	                <configuration>
-	                    <getRevisionOnlyOnce>true</getRevisionOnlyOnce>
-	                </configuration>
-	            </plugin>
+                <plugin>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>buildnumber-maven-plugin</artifactId>
+                    <version>1.4</version>
+                    <executions>
+                        <execution>
+                            <goals>
+                                <goal>create</goal>
+                            </goals>
+                        </execution>
+                    </executions>
+                    <configuration>
+                        <getRevisionOnlyOnce>true</getRevisionOnlyOnce>
+                    </configuration>
+                </plugin>
             </plugins>
         </pluginManagement>
     </build>
@@ -329,6 +330,11 @@ Contributors:
                 <artifactId>leshan-server-cf</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>leshan-server-cluster</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>${project.groupId}</groupId>
                 <artifactId>leshan-client-cf</artifactId>
@@ -354,6 +360,11 @@ Contributors:
                 <artifactId>gson</artifactId>
                 <version>2.2.4</version>
             </dependency>
+            <dependency>
+                <groupId>com.eclipsesource.minimal-json</groupId>
+                <artifactId>minimal-json</artifactId>
+                <version>0.9.4</version>
+            </dependency>
             <dependency>
                 <groupId>commons-lang</groupId>
                 <artifactId>commons-lang</artifactId>


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java b/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java
index 405f3aea..05298bc7 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java
@@ -26,8 +26,8 @@ public class LinkObjectTest {
 
     @Test
     public void parse_with_some_attributes() {
-        LinkObject[] parse = LinkObject.parse("</>;rt=\"oma.lwm2m\";ct=100, </1/101>,</1/102>, </2/0>, </2/1> ;empty"
-                .getBytes());
+        LinkObject[] parse = LinkObject
+                .parse("</>;rt=\"oma.lwm2m\";ct=100, </1/101>,</1/102>, </2/0>, </2/1> ;empty".getBytes());
         Assert.assertEquals(5, parse.length);
         Assert.assertEquals("/", parse[0].getUrl());
 
@@ -38,22 +38,13 @@ public class LinkObjectTest {
 
         Assert.assertEquals("/1/101", parse[1].getUrl());
         Assert.assertEquals(Collections.EMPTY_MAP, parse[1].getAttributes());
-        Assert.assertEquals(Integer.valueOf(1), parse[1].getObjectId());
-        Assert.assertEquals(Integer.valueOf(101), parse[1].getObjectInstanceId());
-        Assert.assertNull(parse[1].getResourceId());
 
         Assert.assertEquals("/1/102", parse[2].getUrl());
         Assert.assertEquals(Collections.EMPTY_MAP, parse[2].getAttributes());
-        Assert.assertEquals(Integer.valueOf(1), parse[2].getObjectId());
-        Assert.assertEquals(Integer.valueOf(102), parse[2].getObjectInstanceId());
-        Assert.assertNull(parse[2].getResourceId());
 
         Assert.assertEquals("/2/0", parse[3].getUrl());
         Assert.assertEquals(Collections.EMPTY_MAP, parse[3].getAttributes());
         Assert.assertEquals("/2/1", parse[4].getUrl());
-        Assert.assertEquals(Integer.valueOf(2), parse[4].getObjectId());
-        Assert.assertEquals(Integer.valueOf(1), parse[4].getObjectInstanceId());
-        Assert.assertNull(parse[4].getResourceId());
 
         attResult = new HashMap<>();
         attResult.put("empty", null);
@@ -73,9 +64,6 @@ public class LinkObjectTest {
         attResult.put("k3", "noquotes");
         attResult.put("k4", "\"startwithquotes");
         Assert.assertEquals(attResult, parse[0].getAttributes());
-        Assert.assertNull(parse[0].getObjectId());
-        Assert.assertNull(parse[0].getObjectInstanceId());
-        Assert.assertNull(parse[0].getResourceId());
     }
 
     @Test
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
index d6611a6d..61641c1c 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
@@ -27,6 +27,7 @@ import org.eclipse.californium.core.network.CoapEndpoint;
 import org.eclipse.californium.core.network.Endpoint;
 import org.eclipse.californium.core.network.config.NetworkConfig;
 import org.eclipse.californium.core.network.config.NetworkConfig.Keys;
+import org.eclipse.californium.core.observe.InMemoryObservationStore;
 import org.eclipse.leshan.LwM2mId;
 import org.eclipse.leshan.client.object.Device;
 import org.eclipse.leshan.client.object.Security;
@@ -39,15 +40,14 @@ import org.eclipse.leshan.integration.tests.util.QueueModeLeshanServer;
 import org.eclipse.leshan.integration.tests.util.QueuedModeLeshanClient;
 import org.eclipse.leshan.server.LwM2mServer;
 import org.eclipse.leshan.server.californium.impl.CaliforniumLwM2mRequestSender;
+import org.eclipse.leshan.server.californium.impl.CaliforniumObservationRegistryImpl;
 import org.eclipse.leshan.server.californium.impl.RegisterResource;
 import org.eclipse.leshan.server.client.Client;
 import org.eclipse.leshan.server.client.ClientRegistry;
 import org.eclipse.leshan.server.impl.ClientRegistryImpl;
-import org.eclipse.leshan.server.impl.ObservationRegistryImpl;
 import org.eclipse.leshan.server.impl.SecurityRegistryImpl;
 import org.eclipse.leshan.server.model.LwM2mModelProvider;
 import org.eclipse.leshan.server.model.StandardModelProvider;
-import org.eclipse.leshan.server.observation.ObservationRegistry;
 import org.eclipse.leshan.server.queue.QueuedRequestFactory;
 import org.eclipse.leshan.server.queue.impl.InMemoryMessageStore;
 import org.eclipse.leshan.server.queue.impl.QueuedRequestFactoryImpl;
@@ -114,8 +114,13 @@ public class QueueModeIntegrationTestHelper extends IntegrationTestHelper {
         createCoapServer(clientRegistry, securityRegistry);
         final InMemoryMessageStore inMemoryMessageStore = new InMemoryMessageStore();
         final QueuedRequestFactory queuedRequestFactory = new QueuedRequestFactoryImpl();
-        final ObservationRegistry observationRegistry = new ObservationRegistryImpl();
         final LwM2mModelProvider modelProvider = new StandardModelProvider();
+        final CaliforniumObservationRegistryImpl observationRegistry = new CaliforniumObservationRegistryImpl(
+                new InMemoryObservationStore(), clientRegistry, modelProvider);
+        observationRegistry.setSecureEndpoint(secureEndpoint);
+        secureEndpoint.addNotificationListener(observationRegistry);
+        observationRegistry.setNonSecureEndpoint(noSecureEndpoint);
+        noSecureEndpoint.addNotificationListener(observationRegistry);
         final LwM2mRequestSender delegateSender = new CaliforniumLwM2mRequestSender(
                 new HashSet<>(coapServer.getEndpoints()), observationRegistry, modelProvider);
         final QueuedRequestSender requestSender = QueuedRequestSender.builder().setMessageStore(inMemoryMessageStore)
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
index 1efc63a1..1a793b93 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
@@ -15,144 +15,121 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.californium.impl;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
+import java.util.Random;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 
-import org.eclipse.californium.core.coap.CoAP.ResponseCode;
 import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.leshan.core.model.LwM2mModel;
-import org.eclipse.leshan.core.model.ObjectLoader;
-import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.californium.core.observe.InMemoryObservationStore;
 import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.observation.ObservationListener;
-import org.eclipse.leshan.server.impl.ObservationRegistryImpl;
+import org.eclipse.leshan.server.californium.CaliforniumObservationRegistry;
+import org.eclipse.leshan.server.impl.ClientRegistryImpl;
+import org.eclipse.leshan.server.model.StandardModelProvider;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class CaliforniumObservationTest {
 
-    final String reportedValue = "15";
     Request coapRequest;
     LwM2mPath target;
+    CaliforniumObservationRegistry registry;
 
     private CaliforniumTestSupport support = new CaliforniumTestSupport();
 
-    private static LwM2mModel model;
-
-    @BeforeClass
-    public static void loadModel() {
-        model = new LwM2mModel(ObjectLoader.loadDefault());
-    }
-
     @Before
     public void setUp() throws Exception {
         support.givenASimpleClient();
-        target = new LwM2mPath(3, 0, 15);
+        registry = new CaliforniumObservationRegistryImpl(new InMemoryObservationStore(), new ClientRegistryImpl(),
+                new StandardModelProvider());
     }
 
     @Test
-    public void coapNotification_is_forwarded_to_observationListener() {
-
-        ObservationListener listener = new ObservationListener() {
-            @Override
-            public void newValue(Observation observation, LwM2mNode value) {
-                assertEquals(CaliforniumObservationTest.this.reportedValue, ((LwM2mResource) value).getValue());
-                assertEquals((Integer) 3, observation.getPath().getObjectId());
-                assertEquals((Integer) 0, observation.getPath().getObjectInstanceId());
-                assertEquals((Integer) 15, observation.getPath().getResourceId());
-                assertEquals(CaliforniumObservationTest.this.support.client.getRegistrationId(),
-                        observation.getRegistrationId());
-            }
-
-            @Override
-            public void cancelled(Observation observation) {
-            }
-        };
-        givenAnObserveRequest(target);
-        CaliforniumObservation observation = new CaliforniumObservation(coapRequest, support.client.getRegistrationId(),
-                target, model);
-        observation.addListener(listener);
-        Response coapResponse = new Response(ResponseCode.CONTENT);
-        coapResponse.setPayload(reportedValue);
-        observation.onResponse(coapResponse);
-    }
+    public void cancel_by_client() {
+        // create some observations and add it to registry
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 13));
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 12));
 
-    @Test
-    public void cancel_Observation_cancel_coapRequest() {
-
-        givenAnObserveRequest(target);
-        assertFalse(coapRequest.isCanceled());
-        CaliforniumObservation observation = new CaliforniumObservation(coapRequest, support.client.getRegistrationId(),
-                target, model);
-        observation.cancel();
-        Assert.assertTrue(coapRequest.isCanceled());
+        givenAnObservation("anotherClient", new LwM2mPath(3, 0, 12));
+
+        // check its presence
+        Set<Observation> observations = registry.getObservations(support.client);
+        Assert.assertEquals(2, observations.size());
+
+        // cancel it
+        int nbCancelled = registry.cancelObservations(support.client);
+        Assert.assertEquals(2, nbCancelled);
+
+        // check its absence
+        observations = registry.getObservations(support.client);
+        Assert.assertTrue(observations.isEmpty());
     }
 
     @Test
-    public void cancelled_Observation_are_removed_from_registry() {
-        // create an observation
-        givenAnObserveRequest(target);
-        CaliforniumObservation observation = new CaliforniumObservation(coapRequest, support.client.getRegistrationId(),
-                target, model);
-        coapRequest.addMessageObserver(observation);
-
-        // add it to registry
-        ObservationRegistryImpl registry = new ObservationRegistryImpl();
-        registry.addObservation(observation);
+    public void cancel_by_path() {
+        // create some observations and add it to registry
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 13));
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 12));
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 12));
+
+        givenAnObservation("anotherClient", new LwM2mPath(3, 0, 12));
 
         // check its presence
         Set<Observation> observations = registry.getObservations(support.client);
-        Assert.assertFalse(observations.isEmpty());
-        Assert.assertFalse(coapRequest.isCanceled());
+        Assert.assertEquals(3, observations.size());
 
         // cancel it
-        observation.cancel();
-        Assert.assertTrue(coapRequest.isCanceled());
+        int nbCancelled = registry.cancelObservations(support.client, "/3/0/12");
+        Assert.assertEquals(2, nbCancelled);
 
         // check its absence
         observations = registry.getObservations(support.client);
-        Assert.assertTrue(observations.isEmpty());
+        Assert.assertEquals(1, observations.size());
     }
 
     @Test
-    public void cancelled_from_registry_observation_are_removed_from_registry() {
-        // create an observation
-        givenAnObserveRequest(target);
-        CaliforniumObservation observation = new CaliforniumObservation(coapRequest, support.client.getRegistrationId(),
-                target, model);
-        coapRequest.addMessageObserver(observation);
-
-        // add it to registry
-        ObservationRegistryImpl registry = new ObservationRegistryImpl();
-        registry.addObservation(observation);
+    public void cancel_by_observation() {
+        // create some observations and add it to registry
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 13));
+        givenAnObservation(support.client.getRegistrationId(), new LwM2mPath(3, 0, 12));
+        givenAnObservation("anotherClient", new LwM2mPath(3, 0, 12));
+
+        Observation observationToCancel = givenAnObservation(support.client.getRegistrationId(),
+                new LwM2mPath(3, 0, 12));
 
         // check its presence
         Set<Observation> observations = registry.getObservations(support.client);
-        Assert.assertFalse(observations.isEmpty());
-        Assert.assertFalse(coapRequest.isCanceled());
+        Assert.assertEquals(3, observations.size());
 
         // cancel it
-        registry.cancelObservations(support.client);
-        Assert.assertTrue(coapRequest.isCanceled());
+        registry.cancelObservation(observationToCancel);
 
         // check its absence
         observations = registry.getObservations(support.client);
-        Assert.assertTrue(observations.isEmpty());
+        Assert.assertEquals(2, observations.size());
     }
 
-    private void givenAnObserveRequest(LwM2mPath target) {
+    private Observation givenAnObservation(String registrationId, LwM2mPath target) {
+
         coapRequest = Request.newGet();
+        coapRequest.setToken(createToken());
         coapRequest.getOptions().addUriPath(String.valueOf(target.getObjectId()));
         coapRequest.getOptions().addUriPath(String.valueOf(target.getObjectInstanceId()));
         coapRequest.getOptions().addUriPath(String.valueOf(target.getResourceId()));
         coapRequest.setDestination(support.client.getAddress());
         coapRequest.setDestinationPort(support.client.getPort());
+        Observation observation = new Observation(coapRequest.getToken(), registrationId, target);
+        registry.addObservation(observation);
+        return observation;
+    }
+
+    private byte[] createToken() {
+        Random random = ThreadLocalRandom.current();
+        byte[] token;
+        token = new byte[random.nextInt(8) + 1];
+        // random value
+        random.nextBytes(token);
+        return token;
     }
 }
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/client/ClientSortObjectTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/client/ClientSortObjectTest.java
index 1142a119..782e8e6a 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/client/ClientSortObjectTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/client/ClientSortObjectTest.java
@@ -41,7 +41,7 @@ public class ClientSortObjectTest {
         LinkObject[] res = c.getSortedObjectLinks();
         Assert.assertEquals(3, res.length);
         Assert.assertNull(res[0]);
-        Assert.assertEquals("/0/2", res[1].getPath());
-        Assert.assertEquals("/0/1024/2", res[2].getPath());
+        Assert.assertEquals("/0/2", res[1].getUrl());
+        Assert.assertEquals("/0/1024/2", res[2].getUrl());
     }
 }
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/impl/ObservationRegistryImplTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/impl/ObservationRegistryImplTest.java
deleted file mode 100644
index a37f155a..00000000
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/impl/ObservationRegistryImplTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
- * 
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Sierra Wireless - initial API and implementation
- *******************************************************************************/
-package org.eclipse.leshan.server.impl;
-
-import java.io.IOException;
-
-import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.observation.Observation;
-import org.eclipse.leshan.core.observation.ObservationListener;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class ObservationRegistryImplTest extends BasicTestSupport {
-
-    ObservationRegistryImpl registry;
-
-    @Before
-    public void setUp() throws Exception {
-        registry = new ObservationRegistryImpl();
-    }
-
-    @Test
-    public void add_duplicate_observation() throws IOException {
-        givenASimpleClient();
-
-        Observation obs = new ObservationImpl(client.getRegistrationId(), new LwM2mPath(3, 0, 15));
-        registry.addObservation(obs);
-
-        Observation duplicate = new ObservationImpl(client.getRegistrationId(), new LwM2mPath(3, 0, 15));
-
-        registry.addObservation(duplicate);
-        Assert.assertSame(1, registry.cancelObservations(client));
-    }
-
-    private class ObservationImpl implements Observation {
-
-        private String registrationId;
-        private LwM2mPath path;
-
-        public ObservationImpl(String registrationId, LwM2mPath path) {
-            this.registrationId = registrationId;
-            this.path = path;
-        }
-
-        @Override
-        public String getRegistrationId() {
-            return registrationId;
-        }
-
-        @Override
-        public LwM2mPath getPath() {
-            return path;
-        }
-
-        @Override
-        public void cancel() {
-        }
-
-        @Override
-        public void addListener(ObservationListener listener) {
-        }
-
-        @Override
-        public void removeListener(ObservationListener listener) {
-        }
-    }
-}
diff --git a/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDesTest.java b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDesTest.java
new file mode 100644
index 00000000..531bc086
--- /dev/null
+++ b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/ClientSerDesTest.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster.serialization;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.leshan.LinkObject;
+import org.eclipse.leshan.server.client.Client;
+import org.junit.Test;
+
+public class ClientSerDesTest {
+
+    @Test
+    public void ser_and_des_are_equals() throws Exception {
+        LinkObject[] objs = new LinkObject[2];
+        Map<String, Object> att = new HashMap<>();
+        att.put("ts", new Integer(12));
+        att.put("rt", "test");
+        objs[0] = new LinkObject("/0/1024/2", att);
+        objs[1] = new LinkObject("/0/2");
+
+        Client.Builder builder = new Client.Builder("registrationId", "endpoint", Inet4Address.getByName("127.0.0.1"),
+                1, new InetSocketAddress(212)).objectLinks(objs);
+
+        builder.registrationDate(new Date(100L));
+        builder.lastUpdate(new Date(101L));
+        Client c = builder.build();
+
+        byte[] ser = ClientSerDes.serialize(c);
+        Client c2 = ClientSerDes.deserialize(ser);
+
+        assertEquals(c, c2);
+    }
+}
diff --git a/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDesTest.java b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDesTest.java
new file mode 100644
index 00000000..191f8297
--- /dev/null
+++ b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/cluster/serialization/SecurityInfoSerDesTest.java
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.demo.cluster.serialization;
+
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
+import java.security.KeyFactory;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.KeySpec;
+
+import org.eclipse.leshan.server.security.SecurityInfo;
+import org.eclipse.leshan.util.Hex;
+import org.junit.Test;
+
+public class SecurityInfoSerDesTest {
+
+    @Test
+    public void security_info_psk_ser_des_then_equal() {
+
+        SecurityInfo si = SecurityInfo.newPreSharedKeyInfo("myendPoint", "pskIdentity",
+                Hex.decodeHex("deadbeef".toCharArray()));
+
+        byte[] data = SecurityInfoSerDes.serialize(si);
+        assertEquals("{\"ep\":\"myendPoint\",\"id\":\"pskIdentity\",\"psk\":\"deadbeef\"}", new String(data));
+        assertEquals(si, SecurityInfoSerDes.deserialize(data));
+    }
+
+    @Test
+    public void security_info_rpk_ser_des_then_equal() throws Exception {
+        byte[] publicX = Hex
+                .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray());
+        byte[] publicY = Hex
+                .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray());
+        // Get Elliptic Curve Parameter spec for secp256r1
+        AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC");
+        algoParameters.init(new ECGenParameterSpec("secp256r1"));
+        ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class);
+
+        // Create key specs
+        KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)),
+                parameterSpec);
+
+        SecurityInfo si = SecurityInfo.newRawPublicKeyInfo("myendpoint",
+                KeyFactory.getInstance("EC").generatePublic(publicKeySpec));
+
+        byte[] data = SecurityInfoSerDes.serialize(si);
+
+        assertEquals(
+                "{\"ep\":\"myendpoint\",\"rpk\":{\"x\":\"89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5\",\"y\":\"cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68\",\"params\":\"secp256r1\"}}",
+                new String(data));
+        System.err.println(new String(SecurityInfoSerDes.serialize(SecurityInfoSerDes.deserialize(data))));
+        assertEquals(si, SecurityInfoSerDes.deserialize(data));
+    }
+}