diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/AbstractDocumentNodeState.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/AbstractDocumentNodeState.java
new file mode 100644
index 0000000..2c7b35c
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/AbstractDocumentNodeState.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState;
+import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
+import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+import org.apache.jackrabbit.oak.util.PerfLogger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+
+public abstract class AbstractDocumentNodeState extends AbstractNodeState {
+    private static final PerfLogger perfLogger = new PerfLogger(
+            LoggerFactory.getLogger(AbstractDocumentNodeState.class.getName()
+                    + ".perf"));
+    protected final NodeStateDiffer differ;
+
+    protected AbstractDocumentNodeState(NodeStateDiffer differ) {
+        this.differ = differ;
+    }
+
+    public abstract String getPath();
+
+    public abstract RevisionVector getRevision();
+
+    public abstract RevisionVector getLastRevision();
+
+    public abstract RevisionVector getRootRevision();
+
+    public abstract boolean isFromExternalChange();
+
+    /**
+     * Creates a copy of this {@code DocumentNodeState} with the
+     * {@link #rootRevision} set to the given {@code root} revision. This method
+     * returns {@code this} instance if the given {@code root} revision is
+     * the same as the one in this instance and the {@link #fromExternalChange}
+     * flags are equal.
+     *
+     * @param root the root revision for the copy of this node state.
+     * @param externalChange if the {@link #fromExternalChange} flag must be
+     *                       set on the returned node state.
+     * @return a copy of this node state with the given root revision and
+     *          external change flag.
+     */
+    public abstract AbstractDocumentNodeState withRootRevision(@Nonnull RevisionVector root,
+                                               boolean externalChange);
+
+    public abstract boolean hasNoChildren();
+
+    //--------------------------< NodeState >-----------------------------------
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        } else if (that instanceof AbstractDocumentNodeState) {
+            AbstractDocumentNodeState other = (AbstractDocumentNodeState) that;
+            if (!getPath().equals(other.getPath())) {
+                // path does not match: not equals
+                // (even if the properties are equal)
+                return false;
+            }
+            if (revisionEquals(other)) {
+                return true;
+            }
+            // revision does not match: might still be equals
+        } else if (that instanceof ModifiedNodeState) {
+            ModifiedNodeState modified = (ModifiedNodeState) that;
+            if (modified.getBaseState() == this) {
+                return EqualsDiff.equals(this, modified);
+            }
+        }
+        if (that instanceof NodeState) {
+            return AbstractNodeState.equals(this, (NodeState) that);
+        }
+        return false;
+    }
+
+    @Override
+    public boolean compareAgainstBaseState(NodeState base, NodeStateDiff diff) {
+        if (this == base) {
+            return true;
+        } else if (base == EMPTY_NODE || !base.exists()) {
+            // special case
+            return EmptyNodeState.compareAgainstEmptyState(this, diff);
+        } else if (base instanceof AbstractDocumentNodeState) {
+            AbstractDocumentNodeState mBase = (AbstractDocumentNodeState) base;
+            if (getPath().equals(mBase.getPath())){
+                if (revisionEquals(mBase)) {
+                    // no differences
+                    return true;
+                } else {
+                    // use DocumentNodeStore compare
+                    final long start = perfLogger.start();
+                    try {
+                        return differ.compare(this, mBase, diff);
+                    } finally {
+                        if (start > 0) {
+                            perfLogger
+                                    .end(start,
+                                            1,
+                                            "compareAgainstBaseState, path={}, readRevision={}, lastRevision={}, base.path={}, base.readRevision={}, base.lastRevision={}",
+                                            getPath(), getRevision(), getLastRevision(),
+                                            mBase.getPath(), mBase.getRevision(),
+                                            mBase.getLastRevision());
+                        }
+                    }
+                }
+            }
+        }
+        // fall back to the generic node state diff algorithm
+        return super.compareAgainstBaseState(base, diff);
+    }
+
+    //------------------------------< internal >--------------------------------
+
+    /**
+     * Returns {@code true} if this state has the same revision as the
+     * {@code other} state. This method first compares the {@link #readRevision}
+     * and then the {@link #lastRevision}.
+     *
+     * @param other the other state to compare with.
+     * @return {@code true} if the revisions are equal, {@code false} otherwise.
+     */
+    private boolean revisionEquals(AbstractDocumentNodeState other) {
+        return this.getRevision().equals(other.getRevision())
+                || this.getLastRevision().equals(other.getLastRevision());
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
index 0b56187..9014e37 100644
--- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
@@ -40,11 +40,8 @@ import org.apache.jackrabbit.oak.json.JsonSerializer;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
-import org.apache.jackrabbit.oak.plugins.memory.ModifiedNodeState;
 import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry;
-import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
 import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
-import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
@@ -57,12 +54,11 @@ import com.google.common.collect.Iterators;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.jackrabbit.oak.commons.StringUtils.estimateMemoryUsage;
-import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
 
 /**
  * A {@link NodeState} implementation for the {@link DocumentNodeStore}.
  */
-public class DocumentNodeState extends AbstractNodeState implements CacheValue {
+public class DocumentNodeState extends AbstractDocumentNodeState implements CacheValue {
 
     private static final PerfLogger perfLogger = new PerfLogger(
             LoggerFactory.getLogger(DocumentNodeState.class.getName()
@@ -110,6 +106,7 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
                               @Nullable RevisionVector lastRevision,
                               @Nullable RevisionVector rootRevision,
                               boolean fromExternalChange) {
+        super(store);
         this.store = checkNotNull(store);
         this.path = checkNotNull(path);
         this.readRevision = checkNotNull(readRevision);
@@ -133,7 +130,8 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
      * @return a copy of this node state with the given root revision and
      *          external change flag.
      */
-    private DocumentNodeState withRootRevision(@Nonnull RevisionVector root,
+    @Override
+    public DocumentNodeState withRootRevision(@Nonnull RevisionVector root,
                                                boolean externalChange) {
         if (rootRevision.equals(root) && fromExternalChange == externalChange) {
             return this;
@@ -157,12 +155,16 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
      * @return {@code true} if this node state was created as a result of an
      *          external change; {@code false} otherwise.
      */
-    boolean isFromExternalChange() {
+    @Override
+    public boolean isFromExternalChange() {
         return fromExternalChange;
     }
 
+    //--------------------------< AbstractDocumentNodeState >-----------------------------------
+
+    @Override
     @Nonnull
-    RevisionVector getRevision() {
+    public RevisionVector getRevision() {
         return readRevision;
     }
 
@@ -178,39 +180,23 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
      *          same value as returned by {@link #getRevision()}.
      */
     @Nonnull
-    RevisionVector getRootRevision() {
+    public RevisionVector getRootRevision() {
         return rootRevision;
     }
 
-    //--------------------------< NodeState >-----------------------------------
+    @Override
+    public String getPath() {
+        return path;
+    }
 
     @Override
-    public boolean equals(Object that) {
-        if (this == that) {
-            return true;
-        } else if (that instanceof DocumentNodeState) {
-            DocumentNodeState other = (DocumentNodeState) that;
-            if (!getPath().equals(other.getPath())) {
-                // path does not match: not equals
-                // (even if the properties are equal)
-                return false;
-            }
-            if (revisionEquals(other)) {
-                return true;
-            }
-            // revision does not match: might still be equals
-        } else if (that instanceof ModifiedNodeState) {
-            ModifiedNodeState modified = (ModifiedNodeState) that;
-            if (modified.getBaseState() == this) {
-                return EqualsDiff.equals(this, modified);
-            }
-        }
-        if (that instanceof NodeState) {
-            return AbstractNodeState.equals(this, (NodeState) that);
-        }
-        return false;
+    public RevisionVector getLastRevision() {
+        return lastRevision;
     }
 
+    //--------------------------< NodeState >-----------------------------------
+
+
     @Override
     public boolean exists() {
         return true;
@@ -238,7 +224,7 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
             return false;
         } else {
             String p = PathUtils.concat(getPath(), name);
-            return store.getNode(p, lastRevision) != null;
+            return store.getNode(p, rootRevision, lastRevision) != null;
         }
     }
 
@@ -250,7 +236,7 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
             return EmptyNodeState.MISSING_NODE;
         }
         String p = PathUtils.concat(getPath(), name);
-        DocumentNodeState child = store.getNode(p, lastRevision);
+        AbstractDocumentNodeState child = store.getNode(p, rootRevision, lastRevision);
         if (child == null) {
             checkValidName(name);
             return EmptyNodeState.MISSING_NODE;
@@ -323,42 +309,6 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
         }
     }
 
-    @Override
-    public boolean compareAgainstBaseState(NodeState base, NodeStateDiff diff) {
-        if (this == base) {
-            return true;
-        } else if (base == EMPTY_NODE || !base.exists()) {
-            // special case
-            return EmptyNodeState.compareAgainstEmptyState(this, diff);
-        } else if (base instanceof DocumentNodeState) {
-            DocumentNodeState mBase = (DocumentNodeState) base;
-            if (store == mBase.store) {
-                if (getPath().equals(mBase.getPath())) {
-                    if (revisionEquals(mBase)) {
-                        // no differences
-                        return true;
-                    } else {
-                        // use DocumentNodeStore compare
-                        final long start = perfLogger.start();
-                        try {
-                            return store.compare(this, mBase, diff);
-                        } finally {
-                            perfLogger
-                                    .end(start,
-                                            1,
-                                            "compareAgainstBaseState, path={}, readRevision={}, lastRevision={}, base.path={}, base.readRevision={}, base.lastRevision={}",
-                                            path, readRevision, lastRevision,
-                                            mBase.path, mBase.readRevision,
-                                            mBase.lastRevision);
-                        }
-                    }
-                }
-            }
-        }
-        // fall back to the generic node state diff algorithm
-        return super.compareAgainstBaseState(base, diff);
-    }
-
     void setProperty(String propertyName, String value) {
         if (value == null) {
             properties.remove(propertyName);
@@ -392,7 +342,8 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
         newNode.properties.putAll(properties);
     }
 
-    boolean hasNoChildren() {
+    @Override
+    public boolean hasNoChildren() {
         return !hasChildren;
     }
 
@@ -426,10 +377,6 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
         return op;
     }
 
-    String getPath() {
-        return path;
-    }
-
     String getId() {
         return path + "@" + lastRevision;
     }
@@ -447,10 +394,6 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
         this.lastRevision = lastRevision;
     }
 
-    RevisionVector getLastRevision() {
-        return lastRevision;
-    }
-
     @Override
     public int getMemory() {
         int size = 40 // shallow
@@ -512,10 +455,10 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
     @Nonnull
     private Iterable<ChildNodeEntry> getChildNodeEntries(@Nullable String name,
                                                          int limit) {
-        Iterable<DocumentNodeState> children = store.getChildNodes(this, name, limit);
-        return Iterables.transform(children, new Function<DocumentNodeState, ChildNodeEntry>() {
+        Iterable<? extends AbstractDocumentNodeState> children = store.getChildNodes(this, name, limit);
+        return Iterables.transform(children, new Function<AbstractDocumentNodeState, ChildNodeEntry>() {
             @Override
-            public ChildNodeEntry apply(final DocumentNodeState input) {
+            public ChildNodeEntry apply(final AbstractDocumentNodeState input) {
                 return new AbstractChildNodeEntry() {
                     @Nonnull
                     @Override
@@ -695,7 +638,7 @@ public class DocumentNodeState extends AbstractNodeState implements CacheValue {
                     return false;
                 } else if (current.hasNext()) {
                     return true;
-                } else if (currentRemaining > 0) {
+                } else if (currentRemaining != 0) {
                     // current returned less than fetchSize
                     return false;
                 }
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStateCache.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStateCache.java
new file mode 100644
index 0000000..08b0d28
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStateCache.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public interface DocumentNodeStateCache {
+    DocumentNodeStateCache NOOP = new DocumentNodeStateCache() {
+        @Nonnull
+        @Override
+        public NodeStateCacheEntry getDocumentNodeState(String path, @Nullable RevisionVector rootRevision,
+                                                        RevisionVector parentLastRev) {
+            return UNKNOWN;
+        }
+    };
+
+    NodeStateCacheEntry MISSING = new NodeStateCacheEntry(NodeStateCacheEntry.EntryType.MISSING);
+
+    NodeStateCacheEntry UNKNOWN = new NodeStateCacheEntry(NodeStateCacheEntry.EntryType.UNKNOWN);
+
+    /**
+     * Get the node for the given path and revision.
+     *
+     * @param path the path of the node.
+     * @param rootRevision
+     * @param readRevision the read revision.
+     * @return the node or {@link MISSING} if no state is there for given path and revision or {@link UNKNOWN} if
+     * cache does not have any knowledge of nodeState for given parameters
+     */
+    @Nonnull
+    NodeStateCacheEntry getDocumentNodeState(String path, RevisionVector rootRevision,
+                                             RevisionVector parentLastRev);
+
+    class NodeStateCacheEntry {
+        private enum EntryType {FOUND, MISSING, UNKNOWN}
+        private final AbstractDocumentNodeState state;
+        private final EntryType entryType;
+
+        public NodeStateCacheEntry(AbstractDocumentNodeState state) {
+            this.state = state;
+            this.entryType = EntryType.FOUND;
+        }
+
+        private NodeStateCacheEntry(EntryType entryType) {
+            this.state = null;
+            this.entryType = entryType;
+        }
+
+        public AbstractDocumentNodeState getState(){
+            checkState(entryType == EntryType.FOUND, "Cannot read state from an entry of type [%s]", entryType);
+            return state;
+        }
+
+        public boolean isUnknown(){
+            return entryType == EntryType.UNKNOWN;
+        }
+
+        public boolean isMissing(){
+            return entryType == EntryType.MISSING;
+        }
+
+        public boolean isFound(){
+            return entryType == EntryType.FOUND;
+        }
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
index 97974e2..bf1bb93 100644
--- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
@@ -85,6 +85,7 @@ import org.apache.jackrabbit.oak.plugins.blob.BlobStoreBlob;
 import org.apache.jackrabbit.oak.plugins.blob.MarkSweepGarbageCollector;
 import org.apache.jackrabbit.oak.plugins.blob.ReferencedBlob;
 import org.apache.jackrabbit.oak.plugins.document.Branch.BranchCommit;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache.NodeStateCacheEntry;
 import org.apache.jackrabbit.oak.plugins.document.cache.CacheInvalidationStats;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCache;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.broadcast.DynamicBroadcastConfig;
@@ -124,7 +125,7 @@ import org.slf4j.LoggerFactory;
  * Implementation of a NodeStore on {@link DocumentStore}.
  */
 public final class DocumentNodeStore
-        implements NodeStore, RevisionContext, Observable, Clusterable {
+        implements NodeStore, RevisionContext, Observable, Clusterable, NodeStateDiffer {
 
     private static final Logger LOG = LoggerFactory.getLogger(DocumentNodeStore.class);
 
@@ -409,6 +410,8 @@ public final class DocumentNodeStore
 
     private final boolean readOnlyMode;
 
+    private DocumentNodeStateCache nodeStateCache = DocumentNodeStateCache.NOOP;
+
     public DocumentNodeStore(DocumentMK.Builder builder) {
         this.blobStore = builder.getBlobStore();
         if (builder.isUseSimpleRevision()) {
@@ -859,6 +862,26 @@ public final class DocumentNodeStore
         }
     }
 
+    @CheckForNull
+    AbstractDocumentNodeState getNode(@Nonnull final String path,
+                              @Nonnull final RevisionVector rootRevision,
+                              @Nonnull final RevisionVector rev) {
+        AbstractDocumentNodeState result = nodeCache.getIfPresent(new PathRev(path, rev));
+        if (result == null){
+            NodeStateCacheEntry entry  = nodeStateCache.getDocumentNodeState(path, rootRevision, rev);
+            if (entry.isMissing()){
+                return null;
+            } else if (entry.isFound()){
+                return entry.getState();
+            }
+        } else {
+            return result == missing
+                    || result.equals(missing) ? null : result;
+        }
+
+        return getNode(path, rev);
+    }
+
     /**
      * Get the node for the given path and revision. The returned object might
      * not be modified directly.
@@ -901,7 +924,7 @@ public final class DocumentNodeStore
         }
     }
 
-    DocumentNodeState.Children getChildren(@Nonnull final DocumentNodeState parent,
+    DocumentNodeState.Children getChildren(@Nonnull final AbstractDocumentNodeState parent,
                               @Nullable final String name,
                               final int limit)
             throws DocumentStoreException {
@@ -950,7 +973,7 @@ public final class DocumentNodeStore
      * @param limit the maximum number of child nodes to return.
      * @return the children of {@code parent}.
      */
-    DocumentNodeState.Children readChildren(DocumentNodeState parent,
+    DocumentNodeState.Children readChildren(AbstractDocumentNodeState parent,
                                             String name, int limit) {
         String queriedName = name;
         String path = parent.getPath();
@@ -1485,8 +1508,9 @@ public final class DocumentNodeStore
      *         {@code false} if it was aborted as requested by the handler
      *         (see the {@link NodeStateDiff} contract for more details)
      */
-    boolean compare(@Nonnull final DocumentNodeState node,
-                    @Nonnull final DocumentNodeState base,
+    @Override
+    public boolean compare(@Nonnull final AbstractDocumentNodeState node,
+                    @Nonnull final AbstractDocumentNodeState base,
                     @Nonnull NodeStateDiff diff) {
         if (!AbstractNodeState.comparePropertiesAgainstBaseState(node, base, diff)) {
             return false;
@@ -2178,8 +2202,8 @@ public final class DocumentNodeStore
     }
 
     private boolean dispatch(@Nonnull final String jsonDiff,
-                             @Nonnull final DocumentNodeState node,
-                             @Nonnull final DocumentNodeState base,
+                             @Nonnull final AbstractDocumentNodeState node,
+                             @Nonnull final AbstractDocumentNodeState base,
                              @Nonnull final NodeStateDiff diff) {
         return DiffCache.parseJsopDiff(jsonDiff, new DiffCache.Diff() {
             @Override
@@ -2258,7 +2282,7 @@ public final class DocumentNodeStore
         return false;
     }
 
-    private String diffImpl(DocumentNodeState from, DocumentNodeState to)
+    private String diffImpl(AbstractDocumentNodeState from, AbstractDocumentNodeState to)
             throws DocumentStoreException {
         JsopWriter w = new JsopStream();
         // TODO this does not work well for large child node lists
@@ -2778,4 +2802,8 @@ public final class DocumentNodeStore
     public String getInstanceId() {
         return String.valueOf(getClusterId());
     }
+
+    public void setNodeStateCache(DocumentNodeStateCache nodeStateCache) {
+        this.nodeStateCache = nodeStateCache;
+    }
 }
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
index 26fe5d0..0e2faf8 100644
--- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
@@ -265,7 +265,7 @@ public class DocumentNodeStoreService {
 
     private final Logger log = LoggerFactory.getLogger(this.getClass());
 
-    private ServiceRegistration reg;
+    private ServiceRegistration nodeStoreReg;
     private final List<Registration> registrations = new ArrayList<Registration>();
     private WhiteboardExecutor executor;
 
@@ -285,6 +285,10 @@ public class DocumentNodeStoreService {
     )
     private volatile DataSource blobDataSource;
 
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY,
+            policy = ReferencePolicy.DYNAMIC)
+    private volatile DocumentNodeStateCache nodeStateCache;
+
     private DocumentMK mk;
     private ObserverTracker observerTracker;
     private ComponentContext context;
@@ -345,6 +349,12 @@ public class DocumentNodeStoreService {
 
     private boolean customBlobStore;
 
+    private DocumentNodeStore documentNodeStore;
+
+    private ServiceRegistration blobStoreReg;
+
+    private BlobStore defaultBlobStore;
+
     @Activate
     protected void activate(ComponentContext context, Map<String, ?> config) throws Exception {
         this.context = context;
@@ -468,6 +478,13 @@ public class DocumentNodeStoreService {
             log.info("Connected to database '{}'", db);
         }
 
+        if (!customBlobStore){
+            defaultBlobStore = mkBuilder.getBlobStore();
+            log.info("Registering the BlobStore with ServiceRegistry");
+            blobStoreReg = context.getBundleContext().registerService(BlobStore.class.getName(),
+                    defaultBlobStore , null);
+        }
+
         //Set wrapping blob store after setting the DB
         if (wrappingCustomBlobStore) {
             ((BlobStoreWrapper) blobStore).setBlobStore(mkBuilder.getBlobStore());
@@ -501,9 +518,9 @@ public class DocumentNodeStoreService {
         registerJournalGC(mk.getNodeStore());
 
         NodeStore store;
-        DocumentNodeStore mns = mk.getNodeStore();
-        store = mns;
-        observerTracker = new ObserverTracker(mns);
+        documentNodeStore = mk.getNodeStore();
+        store = documentNodeStore;
+        observerTracker = new ObserverTracker(documentNodeStore);
 
         observerTracker.start(context.getBundleContext());
 
@@ -532,7 +549,7 @@ public class DocumentNodeStoreService {
         // OAK-2844: in order to allow DocumentDiscoveryLiteService to directly
         // require a service DocumentNodeStore (instead of having to do an 'instanceof')
         // the registration is now done for both NodeStore and DocumentNodeStore here.
-        reg = context.getBundleContext().registerService(
+        nodeStoreReg = context.getBundleContext().registerService(
             new String[]{
                  NodeStore.class.getName(), 
                  DocumentNodeStore.class.getName(), 
@@ -552,6 +569,9 @@ public class DocumentNodeStoreService {
 
     @SuppressWarnings("UnusedDeclaration")
     protected void bindBlobStore(BlobStore blobStore) throws IOException {
+        if (defaultBlobStore == blobStore){
+            return;
+        }
         log.info("Initializing DocumentNodeStore with BlobStore [{}]", blobStore);
         this.blobStore = blobStore;
         registerNodeStoreIfPossible();
@@ -589,6 +609,21 @@ public class DocumentNodeStoreService {
         unregisterNodeStore();
     }
 
+    @SuppressWarnings("UnusedDeclaration")
+    protected void bindNodeStateCache(DocumentNodeStateCache nodeStateCache) throws IOException {
+       if (documentNodeStore != null){
+           log.info("Registered DocumentNodeStateCache [{}] with DocumentNodeStore", nodeStateCache);
+           documentNodeStore.setNodeStateCache(nodeStateCache);
+       }
+    }
+
+    @SuppressWarnings("UnusedDeclaration")
+    protected void unbindNodeStateCache(DocumentNodeStateCache nodeStateCache) {
+        if (documentNodeStore != null){
+            documentNodeStore.setNodeStateCache(DocumentNodeStateCache.NOOP);
+        }
+    }
+
     private void unregisterNodeStore() {
         deactivationTimestamp = System.currentTimeMillis();
 
@@ -597,9 +632,16 @@ public class DocumentNodeStoreService {
         }
         registrations.clear();
 
-        if (reg != null) {
-            reg.unregister();
-            reg = null;
+        if (nodeStoreReg != null) {
+            nodeStoreReg.unregister();
+            nodeStoreReg = null;
+        }
+
+        //If we exposed our BlobStore then unregister it *after*
+        //NodeStore service
+        if (blobStoreReg != null){
+            blobStoreReg.unregister();
+            blobStoreReg = null;
         }
 
         if (mk != null) {
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeStateDiffer.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeStateDiffer.java
new file mode 100644
index 0000000..9cd91ec
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/NodeStateDiffer.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+
+public interface NodeStateDiffer {
+    NodeStateDiffer DEFAULT_DIFFER = new NodeStateDiffer() {
+        @Override
+        public boolean compare(@Nonnull AbstractDocumentNodeState node,
+                               @Nonnull AbstractDocumentNodeState base, @Nonnull NodeStateDiff diff) {
+            return node.compareAgainstBaseState(base, diff);
+        }
+    };
+
+
+    /**
+     * Compares the given {@code node} against the {@code base} state and
+     * reports the differences to the {@link NodeStateDiff}.
+     *
+     * @param node the node to compare.
+     * @param base the base node to compare against.
+     * @param diff handler of node state differences
+     * @return {@code true} if the full diff was performed, or
+     *         {@code false} if it was aborted as requested by the handler
+     *         (see the {@link NodeStateDiff} contract for more details)
+     */
+    boolean compare(@Nonnull final AbstractDocumentNodeState node,
+                    @Nonnull final AbstractDocumentNodeState base,
+                    @Nonnull NodeStateDiff diff);
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeState.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeState.java
new file mode 100644
index 0000000..701482c
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeState.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
+
+/**
+ * NodeState wrapper which wraps another NodeState (mostly SegmentNodeState)
+ * so as to expose it as an {@link AbstractDocumentNodeState} by extracting
+ * the meta properties which are stored as hidden properties
+ */
+class DelegatingDocumentNodeState extends AbstractDocumentNodeState {
+    //Hidden props holding DocumentNodeState meta properties
+    static final String PROP_PATH = ":doc-path";
+    static final String PROP_REVISION = ":doc-rev";
+    static final String PROP_LAST_REV = ":doc-lastRev";
+
+    private static final Predicate<PropertyState> NOT_META_PROPS = new Predicate<PropertyState>() {
+        @Override
+        public boolean apply(@Nullable PropertyState input) {
+            return !input.getName().startsWith(":doc-");
+        }
+    };
+
+    private final NodeState delegate;
+    private final RevisionVector rootRevision;
+    private final boolean fromExternalChange;
+    private final RevisionVector lastRevision;
+    private final RevisionVector readRevision;
+    private final String path;
+
+    /**
+     * Wraps a given NodeState as a {@link DelegatingDocumentNodeState} if
+     * it has required meta properties otherwise just returns the passed NodeState
+     *
+     * @param delegate nodeState to wrap
+     * @return wrapped state or original state
+     */
+    public static NodeState wrapIfPossible(NodeState delegate, NodeStateDiffer differ) {
+        if (hasMetaProps(delegate)) {
+            String revVector = getRequiredProp(delegate, PROP_REVISION);
+            return new DelegatingDocumentNodeState(delegate, RevisionVector.fromString(revVector), differ);
+        }
+        return delegate;
+    }
+
+    public static boolean hasMetaProps(NodeState delegate) {
+        return delegate.hasProperty(PROP_REVISION);
+    }
+
+    public static AbstractDocumentNodeState wrap(NodeState delegate, NodeStateDiffer differ) {
+        String revVector = getRequiredProp(delegate, PROP_REVISION);
+        return new DelegatingDocumentNodeState(delegate, RevisionVector.fromString(revVector), differ);
+    }
+
+    public DelegatingDocumentNodeState(NodeState delegate, RevisionVector rootRevision, NodeStateDiffer differ) {
+        this(delegate, rootRevision, false, differ);
+    }
+
+    public DelegatingDocumentNodeState(NodeState delegate, RevisionVector rootRevision,
+                                       boolean fromExternalChange, NodeStateDiffer differ) {
+        super(differ);
+        this.delegate = delegate;
+        this.rootRevision = rootRevision;
+        this.fromExternalChange = fromExternalChange;
+        this.path = getRequiredProp(PROP_PATH);
+        this.readRevision = RevisionVector.fromString(getRequiredProp(PROP_REVISION));
+        this.lastRevision = RevisionVector.fromString(getRequiredProp(PROP_LAST_REV));
+    }
+
+    private DelegatingDocumentNodeState(DelegatingDocumentNodeState original,
+                                        RevisionVector rootRevision, boolean fromExternalChange) {
+        super(original.differ);
+        this.delegate = original.delegate;
+        this.rootRevision = rootRevision;
+        this.fromExternalChange = fromExternalChange;
+        this.path = original.path;
+        this.readRevision = original.readRevision;
+        this.lastRevision = original.lastRevision;
+    }
+
+    //~----------------------------------< AbstractDocumentNodeState >
+
+    @Override
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public RevisionVector getRevision() {
+        return readRevision;
+    }
+
+    @Override
+    public RevisionVector getLastRevision() {
+        return lastRevision;
+    }
+
+    @Override
+    public RevisionVector getRootRevision() {
+        return rootRevision;
+    }
+
+    @Override
+    public boolean isFromExternalChange() {
+        return fromExternalChange;
+    }
+
+    @Override
+    public AbstractDocumentNodeState withRootRevision(@Nonnull RevisionVector root, boolean externalChange) {
+        if (rootRevision.equals(root) && fromExternalChange == externalChange) {
+            return this;
+        } else {
+            return new DelegatingDocumentNodeState(this, root, externalChange);
+        }
+    }
+
+    @Override
+    public boolean hasNoChildren() {
+        //Passing max as 1 so as to minimize any overhead.
+        return delegate.getChildNodeCount(1) == 0;
+    }
+
+    //~----------------------------------< NodeState >
+
+    @Override
+    public boolean exists() {
+        return true;
+    }
+
+    @Nonnull
+    @Override
+    public Iterable<? extends PropertyState> getProperties() {
+        return Iterables.filter(delegate.getProperties(), NOT_META_PROPS);
+    }
+
+    @Override
+    public boolean hasChildNode(@Nonnull String name) {
+        return delegate.hasChildNode(name);
+    }
+
+    @Nonnull
+    @Override
+    public NodeState getChildNode(@Nonnull String name) throws IllegalArgumentException {
+        return decorate(delegate.getChildNode(name));
+    }
+
+    @Nonnull
+    @Override
+    public Iterable<? extends ChildNodeEntry> getChildNodeEntries() {
+        return Iterables.transform(delegate.getChildNodeEntries(), new Function<ChildNodeEntry, ChildNodeEntry>() {
+            @Nullable
+            @Override
+            public ChildNodeEntry apply(@Nullable ChildNodeEntry input) {
+                return new MemoryChildNodeEntry(input.getName(), decorate(input.getNodeState()));
+            }
+        });
+    }
+
+    @Nonnull
+    @Override
+    public NodeBuilder builder() {
+        checkState(!denotesRoot(getPath()), "Builder cannot be opened for root " +
+                "path for state of type [%s]", delegate.getClass());
+        return new MemoryNodeBuilder(this);
+    }
+
+    //~--------------------------------------------< internal >
+
+    private NodeState decorate(NodeState childNode) {
+        if (childNode.exists()) {
+            return new DelegatingDocumentNodeState(childNode, rootRevision, fromExternalChange, differ);
+        }
+        return childNode;
+    }
+
+    private String getRequiredProp(String name){
+        return getRequiredProp(delegate, name);
+    }
+
+    private static String getRequiredProp(NodeState state, String name){
+        return checkNotNull(state.getString(name), "No property [%s] found in [%s]", name, state);
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/PathFilteringDiff.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/PathFilteringDiff.java
new file mode 100644
index 0000000..d282837
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/PathFilteringDiff.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.spi.state.ApplyDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_LAST_REV;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_PATH;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_REVISION;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+
+class PathFilteringDiff extends ApplyDiff {
+    private final PathFilter pathFilter;
+
+    public PathFilteringDiff(NodeBuilder builder, PathFilter pathFilter) {
+        super(builder);
+        this.pathFilter = pathFilter;
+    }
+
+    @Override
+    public boolean childNodeAdded(String name, NodeState after) {
+        AbstractDocumentNodeState afterDoc = asDocumentState(after);
+        String nextPath = afterDoc.getPath();
+        PathFilter.Result result = pathFilter.filter(nextPath);
+        if (result == PathFilter.Result.EXCLUDE){
+            return true;
+        }
+
+        //We avoid this as we need to copy meta properties
+        //super.childNodeAdded(name, after);
+
+        NodeBuilder childBuilder = builder.child(name);
+        copyMetaProperties(afterDoc, childBuilder);
+        return after.compareAgainstBaseState(EMPTY_NODE,
+                new PathFilteringDiff(childBuilder, pathFilter));
+    }
+
+    @Override
+    public boolean childNodeChanged(String name, NodeState before, NodeState after) {
+        AbstractDocumentNodeState afterDoc = asDocumentState(after);
+        String nextPath = afterDoc.getPath();
+        if (pathFilter.filter(nextPath) != PathFilter.Result.EXCLUDE) {
+            NodeBuilder childBuilder = builder.getChildNode(name);
+            copyMetaProperties(afterDoc, childBuilder);
+            return after.compareAgainstBaseState(
+                    before, new PathFilteringDiff(builder.getChildNode(name), pathFilter));
+        }
+        return true;
+    }
+
+    @Override
+    public boolean childNodeDeleted(String name, NodeState before) {
+        String path = asDocumentState(before).getPath();
+        if (pathFilter.filter(path) != PathFilter.Result.EXCLUDE) {
+            return super.childNodeDeleted(name, before);
+        }
+        return true;
+    }
+
+    static void copyMetaProperties(AbstractDocumentNodeState state, NodeBuilder builder) {
+        builder.setProperty(asPropertyState(PROP_REVISION, state.getRevision()));
+        builder.setProperty(asPropertyState(PROP_LAST_REV, state.getLastRevision()));
+        builder.setProperty(createProperty(PROP_PATH, state.getPath()));
+    }
+
+    private static PropertyState asPropertyState(String name, RevisionVector revision) {
+        return createProperty(name, revision.asString());
+    }
+
+    private static AbstractDocumentNodeState asDocumentState(NodeState state){
+        return (AbstractDocumentNodeState) state;
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCache.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCache.java
new file mode 100644
index 0000000..4289119
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCache.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.EvictingQueue;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.stats.MeterStats;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.jackrabbit.oak.stats.StatsOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+class SecondaryStoreCache implements DocumentNodeStateCache, SecondaryStoreRootObserver {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private static final AbstractDocumentNodeState[] EMPTY = new AbstractDocumentNodeState[0];
+    private final NodeStore store;
+    private final PathFilter pathFilter;
+    private final NodeStateDiffer differ;
+    private final MeterStats unknownPaths;
+    private final MeterStats knownPaths;
+    private final MeterStats headRevMatched;
+    private final MeterStats prevRevMatched;
+    private final int maxSize = 10000;
+    private final EvictingQueue<AbstractDocumentNodeState> queue;
+    private volatile AbstractDocumentNodeState[] previousRoots = EMPTY;
+
+    public SecondaryStoreCache(NodeStore store, PathFilter pathFilter, NodeStateDiffer differ) {
+        this(store, pathFilter, StatisticsProvider.NOOP, differ);
+    }
+
+    public SecondaryStoreCache(NodeStore store, PathFilter pathFilter, StatisticsProvider statisticsProvider,
+                               NodeStateDiffer differ) {
+        this.differ = differ;
+        this.store = store;
+        this.pathFilter = pathFilter;
+        this.unknownPaths = statisticsProvider.getMeter("DOCUMENT_CACHE_SEC_UNKNOWN", StatsOptions.DEFAULT);
+        this.knownPaths = statisticsProvider.getMeter("DOCUMENT_CACHE_SEC_KNOWN", StatsOptions.DEFAULT);
+        this.headRevMatched = statisticsProvider.getMeter("DOCUMENT_CACHE_SEC_HEAD", StatsOptions.DEFAULT);
+        this.prevRevMatched = statisticsProvider.getMeter("DOCUMENT_CACHE_SEC_OLD", StatsOptions.DEFAULT);
+        this.queue = EvictingQueue.create(maxSize);
+    }
+
+    @Nonnull
+    @Override
+    public NodeStateCacheEntry getDocumentNodeState(String path, RevisionVector rootRevision,
+                                                    RevisionVector parentLastRev) {
+        //TODO We might need skip the calls if they occur due to SecondaryStoreObserver
+        //doing the diff or in the startup when we try to sync the state
+        PathFilter.Result result = pathFilter.filter(path);
+        if (result != PathFilter.Result.INCLUDE) {
+            unknownPaths.mark();
+            return DocumentNodeStateCache.UNKNOWN;
+        }
+
+        if (!DelegatingDocumentNodeState.hasMetaProps(store.getRoot())){
+            return DocumentNodeStateCache.UNKNOWN;
+        }
+
+        knownPaths.mark();
+        AbstractDocumentNodeState currentRoot = DelegatingDocumentNodeState.wrap(store.getRoot(), differ);
+
+        NodeStateCacheEntry cacheEntryResult = findByMatchingParentLastRev(currentRoot, path,
+                rootRevision, parentLastRev);
+        if (cacheEntryResult != DocumentNodeStateCache.UNKNOWN){
+            return cacheEntryResult;
+        }
+
+        AbstractDocumentNodeState matchingRoot = findMatchingRoot(rootRevision);
+        if (matchingRoot != null){
+            NodeState state = NodeStateUtils.getNode(matchingRoot, path);
+            if (state.exists()){
+                AbstractDocumentNodeState docState = (AbstractDocumentNodeState) state;
+                prevRevMatched.mark();
+                return new NodeStateCacheEntry(docState);
+            } else {
+                return DocumentNodeStateCache.MISSING;
+            }
+        }
+
+        //TODO Check in tail if rootRevision is not in our maintained list of root
+
+        return DocumentNodeStateCache.UNKNOWN;
+    }
+
+    @Nonnull
+    private NodeStateCacheEntry findByMatchingParentLastRev(AbstractDocumentNodeState root, String path,
+                                                            RevisionVector rootRevision, RevisionVector parentLastRev){
+        NodeState parentNodeState = root;
+        NodeState state = root;
+
+        //Get the parent node state
+        for (String name : PathUtils.elements(checkNotNull(path))) {
+            parentNodeState = state;
+            state = state.getChildNode(checkNotNull(name));
+        }
+
+        if (parentNodeState.exists()) {
+            AbstractDocumentNodeState parentDocState = (AbstractDocumentNodeState) parentNodeState;
+            //So parent state exists and matches the expected revision
+            if (parentLastRev.equals(parentDocState.getLastRevision())) {
+                headRevMatched.mark();
+                if (state.exists()) {
+                    AbstractDocumentNodeState stateAtExpectedRootRev =
+                            ((AbstractDocumentNodeState) state).withRootRevision(rootRevision, false);
+                    return new NodeStateCacheEntry(stateAtExpectedRootRev);
+                } else {
+                    return DocumentNodeStateCache.MISSING;
+                }
+            }
+        }
+
+        return DocumentNodeStateCache.UNKNOWN;
+    }
+
+    @CheckForNull
+    private AbstractDocumentNodeState findMatchingRoot(RevisionVector rr) {
+        if (previousRoots.length == 0){
+            return null;
+        }
+        //TODO Binary search
+        AbstractDocumentNodeState latest = previousRoots[previousRoots.length - 1];
+        AbstractDocumentNodeState oldest = previousRoots[0];
+        if (rr.compareTo(latest.getRootRevision()) <= 0
+                && rr.compareTo(oldest.getRootRevision()) >= 0){
+            for (AbstractDocumentNodeState s : previousRoots){
+                if (s.getRootRevision().equals(rr)){
+                    return s;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void contentChanged(@Nonnull AbstractDocumentNodeState root) {
+        synchronized (queue){
+            //TODO Possibly can be improved
+            queue.add(root);
+            previousRoots = queue.toArray(EMPTY);
+        }
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheService.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheService.java
new file mode 100644
index 0000000..5fd9e95
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheService.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.collect.Lists;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserver;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.state.SecondaryNodeStoreProvider;
+import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toBoolean;
+import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toInteger;
+import static org.apache.jackrabbit.oak.commons.PropertiesUtil.toStringArray;
+import static org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils.registerMBean;
+
+@Component(label = "Apache Jackrabbit Oak DocumentNodeStateCache Provider",
+        metatype = true,
+        policy = ConfigurationPolicy.REQUIRE,
+        description = "Configures a DocumentNodeStateCache based on a secondary NodeStore"
+)
+public class SecondaryStoreCacheService {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    /**
+     * Having a reference to BlobStore ensures that DocumentNodeStoreService does register a BlobStore
+     */
+    @Reference
+    private BlobStore blobStore;
+
+    @Reference
+    private SecondaryNodeStoreProvider secondaryNodeStoreProvider;
+
+    @Reference
+    private Executor executor;
+
+    @Reference
+    private StatisticsProvider statisticsProvider;
+
+    /*
+     * Have an optional dependency on DocumentNodeStore such that we do not have hard dependency
+     * on it and DocumentNodeStore can make use of this service even after it has unregistered
+     */
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY,
+            policy = ReferencePolicy.DYNAMIC)
+    private DocumentNodeStore documentNodeStore;
+
+    @Property(unbounded = PropertyUnbounded.ARRAY,
+            label = "Included Paths",
+            description = "List of paths which are to be included in the secondary store",
+            value = {"/"}
+    )
+    private static final String PROP_INCLUDES = "includedPaths";
+
+    @Property(unbounded = PropertyUnbounded.ARRAY,
+            label = "Excluded Paths",
+            description = "List of paths which are to be excluded in the secondary store",
+            value = {}
+    )
+    private static final String PROP_EXCLUDES = "excludedPaths";
+
+
+    private static final boolean PROP_ASYNC_OBSERVER_DEFAULT = true;
+    @Property(
+            boolValue = PROP_ASYNC_OBSERVER_DEFAULT,
+            label = "Async Observation",
+            description = "Enable async observation processing"
+    )
+    private static final String PROP_ASYNC_OBSERVER = "enableAsyncObserver";
+
+    private static final int PROP_OBSERVER_QUEUE_SIZE_DEFAULT = 1000;
+    @Property(
+            intValue = PROP_OBSERVER_QUEUE_SIZE_DEFAULT,
+            label = "Observer queue size",
+            description = "Observer queue size. Used if 'enableAsyncObserver' is set to true"
+    )
+    private static final String PROP_OBSERVER_QUEUE_SIZE = "observerQueueSize";
+
+    private final List<Registration> oakRegs = Lists.newArrayList();
+
+    private final List<ServiceRegistration> regs = Lists.newArrayList();
+
+    private Whiteboard whiteboard;
+
+    private BundleContext bundleContext;
+
+    private PathFilter pathFilter;
+
+    private final MultiplexingNodeStateDiffer differ = new MultiplexingNodeStateDiffer();
+
+    @Activate
+    private void activate(BundleContext context, Map<String, Object> config){
+        bundleContext = context;
+        whiteboard = new OsgiWhiteboard(context);
+        String[] includedPaths = toStringArray(config.get(PROP_INCLUDES), new String[]{"/"});
+        String[] excludedPaths = toStringArray(config.get(PROP_EXCLUDES), new String[]{""});
+
+        pathFilter = new PathFilter(asList(includedPaths), asList(excludedPaths));
+        NodeStore segStore = secondaryNodeStoreProvider.getSecondaryStore();
+
+        SecondaryStoreCache cache = new SecondaryStoreCache(segStore, pathFilter, statisticsProvider, differ);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(segStore, pathFilter,
+                cache, differ, statisticsProvider);
+        registerObserver(observer, config);
+
+        regs.add(bundleContext.registerService(DocumentNodeStateCache.class.getName(), cache, null));
+
+        //TODO Need to see OSGi dynamics. Its possible that DocumentNodeStore works after the cache
+        //gets deregistered but the SegmentNodeState instances might still be in use and that would cause
+        //failure
+    }
+
+    @Deactivate
+    private void deactivate(){
+        for (Registration r : oakRegs){
+            r.unregister();
+        }
+        for (ServiceRegistration r : regs){
+            r.unregister();
+        }
+    }
+
+    PathFilter getPathFilter() {
+        return pathFilter;
+    }
+
+    protected void bindDocumentNodeStore(DocumentNodeStore documentNodeStore){
+        log.info("Registering DocumentNodeStore as the differ");
+        differ.setDelegate(documentNodeStore);
+    }
+
+    protected void unbindDocumentNodeStore(DocumentNodeStore documentNodeStore){
+        differ.setDelegate(NodeStateDiffer.DEFAULT_DIFFER);
+    }
+
+    //~----------------------------------------------------< internal >
+
+    private void registerObserver(Observer observer, Map<String, Object> config) {
+        boolean enableAsyncObserver = toBoolean(config.get(PROP_ASYNC_OBSERVER), PROP_ASYNC_OBSERVER_DEFAULT);
+        int  queueSize = toInteger(config.get(PROP_OBSERVER_QUEUE_SIZE), PROP_OBSERVER_QUEUE_SIZE_DEFAULT);
+        if (enableAsyncObserver){
+            BackgroundObserver bgObserver = new BackgroundObserver(observer, executor, queueSize);
+            oakRegs.add(registerMBean(whiteboard,
+                    BackgroundObserverMBean.class,
+                    bgObserver.getMBean(),
+                    BackgroundObserverMBean.TYPE,
+                    "Secondary NodeStore observer stats"));
+            observer = bgObserver;
+            log.info("Configuring the observer for secondary NodeStore as " +
+                    "Background Observer with queue size {}", queueSize);
+        }
+
+        //Ensure that our observer comes first in processing
+        Hashtable<String, Object> props = new Hashtable<>();
+        props.put(Constants.SERVICE_RANKING, 10000);
+        regs.add(bundleContext.registerService(Observer.class.getName(), observer, props));
+    }
+
+    private static class MultiplexingNodeStateDiffer implements NodeStateDiffer {
+        private volatile NodeStateDiffer delegate = NodeStateDiffer.DEFAULT_DIFFER;
+        @Override
+        public boolean compare(@Nonnull AbstractDocumentNodeState node,
+                               @Nonnull AbstractDocumentNodeState base, @Nonnull NodeStateDiff diff) {
+            return delegate.compare(node, base, diff);
+        }
+
+        public void setDelegate(NodeStateDiffer delegate) {
+            this.delegate = delegate;
+        }
+    }
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserver.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserver.java
new file mode 100644
index 0000000..4f44320
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserver.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.base.Stopwatch;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.state.ApplyDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.jackrabbit.oak.stats.StatsOptions;
+import org.apache.jackrabbit.oak.stats.TimerStats;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class SecondaryStoreObserver implements Observer {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final NodeStore nodeStore;
+    private final PathFilter pathFilter;
+    private final SecondaryStoreRootObserver secondaryObserver;
+    private final NodeStateDiffer differ;
+    private final TimerStats local;
+    private final TimerStats external;
+    private boolean firstEventProcessed;
+
+    public SecondaryStoreObserver(NodeStore nodeStore, PathFilter pathFilter, NodeStateDiffer differ) {
+        this(nodeStore, pathFilter, SecondaryStoreRootObserver.NOOP, differ, StatisticsProvider.NOOP);
+    }
+
+    public SecondaryStoreObserver(NodeStore nodeStore, PathFilter pathFilter,
+                                  SecondaryStoreRootObserver secondaryObserver,
+                                  NodeStateDiffer differ, StatisticsProvider statisticsProvider) {
+        this.nodeStore = nodeStore;
+        this.pathFilter = pathFilter;
+        this.secondaryObserver = secondaryObserver;
+        this.differ = differ;
+        this.local = statisticsProvider.getTimer("DOCUMENT_CACHE_SEC_LOCAL", StatsOptions.DEFAULT);
+        this.external = statisticsProvider.getTimer("DOCUMENT_CACHE_SEC_EXTERNAL", StatsOptions.DEFAULT);
+    }
+
+    @Override
+    public void contentChanged(@Nonnull NodeState root, @Nullable CommitInfo info) {
+        //Diff here would also be traversing non visible areas and there
+        //diffManyChildren might pose problem for e.g. data under uuid index
+        if (!firstEventProcessed){
+            log.info("Starting initial sync");
+        }
+
+        Stopwatch w = Stopwatch.createStarted();
+        NodeState target = root;
+        NodeState secondaryRoot = nodeStore.getRoot();
+        NodeState base = DelegatingDocumentNodeState.wrapIfPossible(secondaryRoot, differ);
+        NodeBuilder builder = secondaryRoot.builder();
+        ApplyDiff diff = new PathFilteringDiff(builder, pathFilter);
+
+        //Copy the root node meta properties
+        PathFilteringDiff.copyMetaProperties((AbstractDocumentNodeState) target, builder);
+
+        //Apply the rest of properties
+        target.compareAgainstBaseState(base, diff);
+        try {
+            NodeState updatedSecondaryRoot = nodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+            secondaryObserver.contentChanged(DelegatingDocumentNodeState.wrap(updatedSecondaryRoot, differ));
+
+            TimerStats timer = info == null ? external : local;
+            timer.update(w.elapsed(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
+
+            if (!firstEventProcessed){
+                log.info("Time taken for initial sync {}", w);
+                firstEventProcessed = true;
+            }
+        } catch (CommitFailedException e) {
+            //TODO
+            log.warn("Commit to secondary store failed", e);
+        }
+    }
+
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreRootObserver.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreRootObserver.java
new file mode 100644
index 0000000..8e0037c
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreRootObserver.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+
+interface SecondaryStoreRootObserver {
+    SecondaryStoreRootObserver NOOP = new SecondaryStoreRootObserver(){
+        @Override
+        public void contentChanged(@Nonnull AbstractDocumentNodeState root) {
+
+        }
+    };
+
+    void contentChanged(@Nonnull AbstractDocumentNodeState root);
+}
diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/SecondaryNodeStoreProvider.java oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/SecondaryNodeStoreProvider.java
new file mode 100644
index 0000000..c0d7325
--- /dev/null
+++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/state/SecondaryNodeStoreProvider.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.spi.state;
+
+public interface SecondaryNodeStoreProvider {
+
+    NodeStore getSecondaryStore();
+}
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
index 2bfc799..3d33c43 100644
--- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
@@ -213,20 +213,25 @@ public class DocumentNodeStoreTest {
         DocumentNodeStore store = builderProvider.newBuilder()
                 .setDocumentStore(docStore).getNodeStore();
         NodeBuilder root = store.getRoot().builder();
-        for (int i = 0; i < 10; i++) {
-            root.child("node-" + i);
+        NodeBuilder builder = root.child("a");
+        int childNodeSize = 300;
+        for (int i = 0; i < childNodeSize; i++) {
+            builder.child("node-" + i);
         }
         store.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
         counter.set(0);
         // the following should just make one call to DocumentStore.query()
-        for (ChildNodeEntry e : store.getRoot().getChildNodeEntries()) {
+        int expectedSize = 0;
+        for (ChildNodeEntry e : store.getRoot().getChildNode("a").getChildNodeEntries()) {
             e.getNodeState();
+            expectedSize++;
         }
-        assertEquals(1, counter.get());
+        System.out.println(expectedSize);
+        assertEquals(0, counter.get());
 
         counter.set(0);
         // now the child node entries are cached and no call should happen
-        for (ChildNodeEntry e : store.getRoot().getChildNodeEntries()) {
+        for (ChildNodeEntry e : store.getRoot().getChildNode("a").getChildNodeEntries()) {
             e.getNodeState();
         }
         assertEquals(0, counter.get());
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeStateTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeStateTest.java
new file mode 100644
index 0000000..ffa1a9e
--- /dev/null
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/DelegatingDocumentNodeStateTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.document.Revision;
+import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.junit.Test;
+
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_LAST_REV;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_PATH;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.DelegatingDocumentNodeState.PROP_REVISION;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class DelegatingDocumentNodeStateTest {
+    private NodeBuilder builder = EMPTY_NODE.builder();
+
+    @Test
+    public void basicWorking() throws Exception{
+        RevisionVector rv1 = new RevisionVector(new Revision(1,0,1));
+        RevisionVector rv2 = new RevisionVector(new Revision(1,0,3));
+        builder.setProperty(asPropertyState(PROP_REVISION, rv1));
+        builder.setProperty(asPropertyState(PROP_LAST_REV, rv2));
+        builder.setProperty(createProperty(PROP_PATH, "foo"));
+        AbstractDocumentNodeState state = DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER);
+
+        assertEquals(rv1, state.getRevision());
+        assertEquals(rv1, state.getRootRevision());
+        assertEquals(rv2, state.getLastRevision());
+        assertEquals("foo", state.getPath());
+        assertTrue(state.hasNoChildren());
+        assertTrue(state.exists());
+        assertFalse(state.isFromExternalChange());
+    }
+    
+    @Test
+    public void metaPropertiesFilteredOut() throws Exception{
+        setMetaProps(builder);
+        builder.setProperty("foo", "bar");
+
+        AbstractDocumentNodeState state = DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER);
+        assertEquals(1, Iterables.size(state.getProperties()));
+    }
+
+    @Test
+    public void childNodeDecorated() throws Exception{
+        setMetaProps(builder);
+        setMetaProps(builder.child("a"));
+        setMetaProps(builder.child("b"));
+
+        AbstractDocumentNodeState state = DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER);
+        assertTrue(state.getChildNode("a") instanceof AbstractDocumentNodeState);
+        assertTrue(state.getChildNode("b") instanceof AbstractDocumentNodeState);
+        assertFalse(state.hasChildNode("c"));
+        assertFalse(state.getChildNode("c").exists());
+
+        assertFalse(state.hasNoChildren());
+
+        for(ChildNodeEntry cne : state.getChildNodeEntries()){
+            assertTrue(cne.getNodeState() instanceof AbstractDocumentNodeState);
+        }
+
+        assertEquals(2, state.getChildNodeCount(100));
+    }
+
+    @Test
+    public void withRootRevision() throws Exception{
+        RevisionVector rv1 = new RevisionVector(new Revision(1,0,1));
+        RevisionVector rv2 = new RevisionVector(new Revision(1,0,3));
+        builder.setProperty(asPropertyState(PROP_REVISION, rv1));
+        builder.setProperty(asPropertyState(PROP_LAST_REV, rv2));
+        builder.setProperty(createProperty(PROP_PATH, "foo"));
+        AbstractDocumentNodeState state = DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER);
+
+        AbstractDocumentNodeState state2 = state.withRootRevision(rv1, false);
+        assertSame(state, state2);
+
+        RevisionVector rv4 = new RevisionVector(new Revision(1,0,4));
+        AbstractDocumentNodeState state3 = state.withRootRevision(rv4, true);
+        assertEquals(rv4, state3.getRootRevision());
+        assertTrue(state3.isFromExternalChange());
+    }
+
+    @Test
+    public void wrapIfPossible() throws Exception{
+        assertFalse(DelegatingDocumentNodeState.wrapIfPossible(EMPTY_NODE, NodeStateDiffer.DEFAULT_DIFFER)
+                instanceof AbstractDocumentNodeState);
+
+        setMetaProps(builder);
+        assertTrue(DelegatingDocumentNodeState.wrapIfPossible(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER) instanceof
+                AbstractDocumentNodeState);
+    }
+
+    @Test
+    public void equals1() throws Exception{
+        setMetaProps(builder);
+        builder.setProperty("foo", "bar");
+
+        NodeBuilder b2 = EMPTY_NODE.builder();
+        b2.setProperty("foo", "bar");
+
+        assertTrue(EqualsDiff.equals(DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER),
+                b2.getNodeState()));
+        assertTrue(EqualsDiff.equals(b2.getNodeState(),
+                DelegatingDocumentNodeState.wrap(builder.getNodeState(), NodeStateDiffer.DEFAULT_DIFFER)));
+    }
+
+    private static void setMetaProps(NodeBuilder nb){
+        nb.setProperty(asPropertyState(PROP_REVISION, new RevisionVector(new Revision(1,0,1))));
+        nb.setProperty(asPropertyState(PROP_LAST_REV, new RevisionVector(new Revision(1,0,1))));
+        nb.setProperty(createProperty(PROP_PATH, "foo"));
+    }
+
+    private static PropertyState asPropertyState(String name, RevisionVector revision) {
+        return createProperty(name, revision.asString());
+    }
+
+}
\ No newline at end of file
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/OakInitTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/OakInitTest.java
new file mode 100644
index 0000000..ec28c62
--- /dev/null
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/OakInitTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.DocumentStore;
+import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.apache.jackrabbit.oak.plugins.document.util.LoggingDocumentStoreWrapper;
+import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.reference.ReferenceEditorProvider;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent;
+import org.apache.jackrabbit.oak.spi.lifecycle.CompositeInitializer;
+import org.apache.jackrabbit.oak.spi.lifecycle.OakInitializer;
+import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+public class OakInitTest {
+    private final List<String> empty = Collections.emptyList();
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore primary;
+    private NodeStore secondary;
+
+    @Before
+    public void setUp(){
+        DocumentStore ds = new LoggingDocumentStoreWrapper(new MemoryDocumentStore());
+        primary = builderProvider.newBuilder().setDocumentStore(ds).getNodeStore();
+        secondary = new MemoryNodeStore();
+    }
+
+    @Test
+    public void oakInit() throws Exception{
+        List<RepositoryInitializer> initializers = newArrayList();
+
+        initializers.add(new InitialContent());
+
+        List<IndexEditorProvider> indexEditorProviders = newArrayList();
+        indexEditorProviders.add(new ReferenceEditorProvider());
+        indexEditorProviders.add(new PropertyIndexEditorProvider());
+        indexEditorProviders.add(new PropertyIndexEditorProvider());
+
+        IndexEditorProvider indexEditors = CompositeIndexEditorProvider.compose(indexEditorProviders);
+        OakInitializer.initialize(primary, new CompositeInitializer(initializers), indexEditors);
+    }
+
+
+}
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheServiceTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheServiceTest.java
new file mode 100644
index 0000000..03fbe62
--- /dev/null
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.blob.BlobStore;
+import org.apache.jackrabbit.oak.spi.blob.MemoryBlobStore;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.state.SecondaryNodeStoreProvider;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class SecondaryStoreCacheServiceTest {
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private SecondaryStoreCacheService cacheService = new SecondaryStoreCacheService();
+    private NodeStore secondaryStore = new MemoryNodeStore();
+
+    @Before
+    public void configureDefaultServices(){
+        context.registerService(BlobStore.class, new MemoryBlobStore());
+        context.registerService(SecondaryNodeStoreProvider.class, new SecondaryNodeStoreProvider() {
+            @Override
+            public NodeStore getSecondaryStore() {
+                return secondaryStore;
+            }
+        });
+        context.registerService(Executor.class, Executors.newSingleThreadExecutor());
+        context.registerService(StatisticsProvider.class, StatisticsProvider.NOOP);
+        MockOsgi.injectServices(cacheService, context.bundleContext());
+    }
+
+    @Test
+    public void defaultSetup() throws Exception{
+        MockOsgi.activate(cacheService, context.bundleContext(), new HashMap<String, Object>());
+
+        assertNotNull(context.getService(Observer.class));
+        assertNotNull(context.getService(BackgroundObserverMBean.class));
+        assertNotNull(context.getService(DocumentNodeStateCache.class));
+
+        MockOsgi.deactivate(cacheService);
+
+        assertNull(context.getService(Observer.class));
+        assertNull(context.getService(BackgroundObserverMBean.class));
+        assertNull(context.getService(DocumentNodeStateCache.class));
+    }
+
+    @Test
+    public void disableBackground() throws Exception{
+        Map<String, Object> config = new HashMap<>();
+        config.put("enableAsyncObserver", "false");
+        MockOsgi.activate(cacheService, context.bundleContext(), config);
+
+        assertNotNull(context.getService(Observer.class));
+        assertNull(context.getService(BackgroundObserverMBean.class));
+        assertNotNull(context.getService(DocumentNodeStateCache.class));
+    }
+
+    @Test
+    public void configurePathFilter() throws Exception{
+        Map<String, Object> config = new HashMap<>();
+        config.put("includedPaths", new String[] {"/a"});
+        config.put("excludedPaths", new String[] {"/a/b"});
+        MockOsgi.activate(cacheService, context.bundleContext(), config);
+
+        assertEquals(PathFilter.Result.INCLUDE, cacheService.getPathFilter().filter("/a"));
+        assertEquals(PathFilter.Result.EXCLUDE, cacheService.getPathFilter().filter("/a/b/c"));
+    }
+
+}
\ No newline at end of file
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheTest.java
new file mode 100644
index 0000000..b1ea97e
--- /dev/null
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreCacheTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStateCache.NodeStateCacheEntry;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.document.Revision;
+import org.apache.jackrabbit.oak.plugins.document.RevisionVector;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer.DEFAULT_DIFFER;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.SecondaryStoreObserverTest.create;
+import static org.apache.jackrabbit.oak.plugins.document.secondary.SecondaryStoreObserverTest.documentState;
+import static org.junit.Assert.*;
+
+public class SecondaryStoreCacheTest {
+    private final List<String> empty = Collections.emptyList();
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore primary;
+    private NodeStore secondary;
+
+    @Before
+    public void setUp() throws IOException {
+        primary = builderProvider.newBuilder().getNodeStore();
+        secondary = new MemoryNodeStore();
+    }
+
+    @Test
+    public void basicTest() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        SecondaryStoreCache cache = new SecondaryStoreCache(secondary, pathFilter, DEFAULT_DIFFER);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        RevisionVector rv1 = new RevisionVector(new Revision(1,0,1));
+        RevisionVector rv2 = new RevisionVector(new Revision(1,0,3));
+        assertSame(DocumentNodeStateCache.UNKNOWN, cache.getDocumentNodeState("/a/b", rv1, rv2));
+        assertSame(DocumentNodeStateCache.UNKNOWN, cache.getDocumentNodeState("/x", rv1, rv2));
+    }
+
+    @Test
+    public void updateAndReadAtReadRev() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        SecondaryStoreCache cache = new SecondaryStoreCache(secondary, pathFilter, DEFAULT_DIFFER);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        AbstractDocumentNodeState r1 =
+                (AbstractDocumentNodeState) primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        //Update some other part of tree i.e. which does not change lastRev for /a/c
+        nb = primary.getRoot().builder();
+        create(nb, "/a/e/d");
+        AbstractDocumentNodeState r2 =
+                (AbstractDocumentNodeState)primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        //Lookup should work fine
+        AbstractDocumentNodeState a_r2 = documentState(r2, "/a");
+        NodeStateCacheEntry result
+                = cache.getDocumentNodeState("/a/c", r2.getRootRevision(), a_r2.getLastRevision());
+        assertTrue(EqualsDiff.equals(a_r2.getChildNode("c"), result.getState()));
+
+        nb = primary.getRoot().builder();
+        nb.child("a").child("c").remove();
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        //Now look from older revision
+        result = cache.getDocumentNodeState("/a/c", r1.getRootRevision(), a_r2.getLastRevision());
+
+        //now as its not visible from head it would not be visible
+        assertSame(DocumentNodeStateCache.UNKNOWN, result);
+    }
+
+    @Test
+    public void updateAndReadAtPrevRevision() throws Exception {
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreCache cache = new SecondaryStoreCache(secondary, pathFilter, DEFAULT_DIFFER);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, cache,
+                DEFAULT_DIFFER, StatisticsProvider.NOOP);
+        primary.addObserver(observer);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c");
+        AbstractDocumentNodeState r0 =
+                (AbstractDocumentNodeState) primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        AbstractDocumentNodeState a_c_0 = documentState(primary.getRoot(), "/a/c");
+
+        //Update some other part of tree i.e. which does not change lastRev for /a/c
+        nb = primary.getRoot().builder();
+        create(nb, "/a/c/d");
+        AbstractDocumentNodeState r1 =
+                (AbstractDocumentNodeState)primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        AbstractDocumentNodeState a_c_1 = documentState(primary.getRoot(), "/a/c");
+
+        NodeStateCacheEntry result
+                = cache.getDocumentNodeState("/a/c", r1.getRootRevision(), a_c_1.getLastRevision());
+        assertTrue(EqualsDiff.equals(a_c_1, result.getState()));
+
+        //Read from older revision
+        result
+                = cache.getDocumentNodeState("/a/c", r0.getRootRevision(), a_c_0.getLastRevision());
+        assertTrue(EqualsDiff.equals(a_c_0, result.getState()));
+    }
+
+}
\ No newline at end of file
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserverTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserverTest.java
new file mode 100644
index 0000000..47f7c7a
--- /dev/null
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/secondary/SecondaryStoreObserverTest.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.secondary;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.document.AbstractDocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.NodeStateDiffer;
+import org.apache.jackrabbit.oak.plugins.index.PathFilter;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class SecondaryStoreObserverTest {
+    private final List<String> empty = Collections.emptyList();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+
+    private DocumentNodeStore primary;
+    private NodeStore secondary;
+
+    @Before
+    public void setUp() throws IOException {
+        primary = builderProvider.newBuilder().getNodeStore();
+        secondary = new MemoryNodeStore();
+    }
+
+    @Test
+    public void basicSetup() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, NodeStateDiffer.DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        dump(secondaryRoot(), "/a");
+        dump(primary.getRoot(), "/a");
+        assertEquals(secondaryRoot().getChildNode("a"),
+                primary.getRoot().getChildNode("a"));
+    }
+
+    @Test
+    public void childNodeAdded() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, NodeStateDiffer.DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        nb = primary.getRoot().builder();
+        create(nb, "/a/d");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        assertMetaState(primary.getRoot(), secondaryRoot(), "/a/d");
+        assertMetaState(primary.getRoot(), secondaryRoot(), "/a");
+    }
+
+    @Test
+    public void childNodeChangedAndExclude() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), of("a/b"));
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, NodeStateDiffer.DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        nb = primary.getRoot().builder();
+        create(nb, "/a/d", "/a/b/e");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        assertMetaState(primary.getRoot(), secondaryRoot(), "/a/d");
+    }
+
+    @Test
+    public void childNodeDeleted() throws Exception{
+        PathFilter pathFilter = new PathFilter(of("/a"), empty);
+        SecondaryStoreObserver observer = new SecondaryStoreObserver(secondary, pathFilter, NodeStateDiffer.DEFAULT_DIFFER);
+        primary.addObserver(observer);
+
+        NodeBuilder nb = primary.getRoot().builder();
+        create(nb, "/a/b", "/a/c", "/x/y/z");
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        nb = primary.getRoot().builder();
+        nb.child("a").child("c").remove();
+        primary.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        assertFalse(NodeStateUtils.getNode(secondaryRoot(), "/a/c").exists());
+    }
+
+    private NodeState secondaryRoot() {
+        return DelegatingDocumentNodeState.wrap(secondary.getRoot(), NodeStateDiffer.DEFAULT_DIFFER);
+    }
+
+    private static void assertMetaState(NodeState root1, NodeState root2, String path){
+        assertMetaState(documentState(root1, path), documentState(root2, path));
+    }
+
+    private static void assertMetaState(AbstractDocumentNodeState a, AbstractDocumentNodeState b){
+        assertEquals(a.getRevision(), b.getRevision());
+        assertEquals(a.getRootRevision(), b.getRootRevision());
+        assertEquals(a.getPath(), b.getPath());
+    }
+
+    static AbstractDocumentNodeState documentState(NodeState root, String path){
+        return (AbstractDocumentNodeState) NodeStateUtils.getNode(root, path);
+    }
+
+    private static void dump(NodeState root, String path){
+        NodeState state = NodeStateUtils.getNode(root, path);
+        System.out.println(NodeStateUtils.toString(state));
+    }
+
+    static NodeState create(NodeBuilder b, String ... paths){
+        for (String path : paths){
+            NodeBuilder cb = b;
+            for (String pathElement : PathUtils.elements(path)){
+                cb = cb.child(pathElement);
+            }
+        }
+        return b.getNodeState();
+    }
+
+}
\ No newline at end of file
diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
index d901d7b..fba3a47 100644
--- oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
+++ oak-core/src/test/java/org/apache/jackrabbit/oak/query/AbstractQueryTest.java
@@ -272,9 +272,9 @@ public abstract class AbstractQueryTest {
             }
         } catch (ParseException e) {
             lines.add(e.toString());
-        } catch (IllegalArgumentException e) {
+        } /*catch (IllegalArgumentException e) {
             lines.add(e.toString());
-        }
+        }*/
         time = System.currentTimeMillis() - time;
         if (time > 10000 && !isDebugModeEnabled()) {
             fail("Query took too long: " + query + " took " + time + " ms");
diff --git oak-core/src/test/resources/logback-test.xml oak-core/src/test/resources/logback-test.xml
index 7a9abc4..44c9d82 100644
--- oak-core/src/test/resources/logback-test.xml
+++ oak-core/src/test/resources/logback-test.xml
@@ -24,6 +24,7 @@
 
 <appender name="file" class="ch.qos.logback.core.FileAppender">
     <file>target/unit-tests.log</file>
+    <append>false</append>
     <encoder>
         <pattern>%date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n</pattern>
     </encoder>
diff --git oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndexTest.java oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndexTest.java
index 4d39037..88a6b57 100644
--- oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndexTest.java
+++ oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LucenePropertyIndexTest.java
@@ -2112,6 +2112,18 @@ public class LucenePropertyIndexTest extends AbstractQueryTest {
         assertPlanAndQuery(query, "lucene:test1(/oak:index/test1)", Collections.<String>emptyList());
     }
 
+    @Test
+    public void plansWithLimit() throws Exception{
+        createIndex("test1", of("propc"));
+
+        Tree test = root.getTree("/").addChild("test");
+        test.addChild("d").setProperty("propc", "foo");
+        root.commit();
+
+        String statement = "select [jcr:path] from [nt:base] where [propa] = 'foo'";
+        qe.executeQuery(statement, "JCR-SQL2", 100, 0, NO_BINDINGS, NO_MAPPINGS);
+    }
+
     private void assertPlanAndQuery(String query, String planExpectation, List<String> paths){
         assertThat(explain(query), containsString(planExpectation));
         assertQuery(query, paths);
diff --git oak-segment/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java oak-segment/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
index 86de37b..462dfee 100644
--- oak-segment/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
+++ oak-segment/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java
@@ -89,6 +89,7 @@ import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.spi.state.ProxyNodeStore;
 import org.apache.jackrabbit.oak.spi.state.RevisionGC;
 import org.apache.jackrabbit.oak.spi.state.RevisionGCMBean;
+import org.apache.jackrabbit.oak.spi.state.SecondaryNodeStoreProvider;
 import org.apache.jackrabbit.oak.spi.whiteboard.CompositeRegistration;
 import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
 import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardExecutor;
@@ -237,6 +238,13 @@ public class SegmentNodeStoreService extends ProxyNodeStore
     )
     public static final String STANDBY = "standby";
 
+    @Property(
+            boolValue = false,
+            label = "Secondary Store Mode",
+            description = "Flag indicating that this component will not register as a NodeStore but just as a SecondaryNodeStoreProvider"
+    )
+    public static final String SECONDARY_STORE = "secondary";
+
     @Property(boolValue = false,
             label = "Custom BlobStore",
             description = "Boolean value indicating that a custom BlobStore is to be used. " +
@@ -347,6 +355,11 @@ public class SegmentNodeStoreService extends ProxyNodeStore
                 return;
             }
 
+            if (toBoolean(property(SECONDARY_STORE), false)){
+                registerSecondaryStore();
+                return;
+            }
+
             if (registerSegmentNodeStore()) {
                 Dictionary<String, Object> props = new Hashtable<String, Object>();
                 props.put(Constants.SERVICE_PID, SegmentNodeStore.class.getName());
@@ -356,6 +369,20 @@ public class SegmentNodeStoreService extends ProxyNodeStore
         }
     }
 
+    private void registerSecondaryStore() {
+        SegmentNodeStore.SegmentNodeStoreBuilder nodeStoreBuilder = SegmentNodeStore.builder(store);
+        nodeStoreBuilder.withCompactionStrategy(compactionStrategy);
+        segmentNodeStore = nodeStoreBuilder.build();
+        storeRegistration = context.getBundleContext().registerService(SecondaryNodeStoreProvider.class.getName(),
+                new SecondaryNodeStoreProvider() {
+                    @Override
+                    public NodeStore getSecondaryStore() {
+                        return SegmentNodeStoreService.this;
+                    }
+                }, null);
+        log.info("Registered SecondaryNodeStoreProvider backed by SegmentNodeStore");
+    }
+
     private boolean registerSegmentStore() throws IOException {
         if (context == null) {
             log.info("Component still not activated. Ignoring the initialization call");
