diff --git a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/sort/StringSort.java b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/sort/StringSort.java
index 6ccbfa6..601af6b 100644
--- a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/sort/StringSort.java
+++ b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/sort/StringSort.java
@@ -35,6 +35,7 @@ import com.google.common.base.Charsets;
 import com.google.common.collect.Lists;
 import com.google.common.io.Closer;
 import com.google.common.io.Files;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.LineIterator;
 import org.slf4j.Logger;
@@ -253,4 +254,18 @@ public class StringSort implements Closeable {
             super(reader);
         }
     }
+
+    public Iterable<String> getIterableIds() {
+        return new Iterable<String>() {
+
+            @Override
+            public Iterator<String> iterator() {
+                try {
+                    return getIds();
+                } catch (IOException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+        };
+    }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
index 3455f4b..844316a 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStore.java
@@ -1801,9 +1801,37 @@ public final class DocumentNodeStore
 
         if (!externalChanges.isEmpty()) {
             // invalidate caches
-            stats.cacheStats = store.invalidateCache();
-            // TODO only invalidate affected items
-            docChildrenCache.invalidateAll();
+            if (externalSort==null) {
+                // if no externalSort available, then invalidate the classic way: everything
+                stats.cacheStats = store.invalidateCache();
+                docChildrenCache.invalidateAll();
+            } else {
+                try {
+                    externalSort.sort();
+                    stats.cacheStats = store.invalidateCache(externalSort.getIterableIds());
+                    // OAK-3002: only invalidate affected items (using journal)
+                    long origSize = docChildrenCache.size();
+                    if (origSize==0) {
+                        // if docChildrenCache is empty, don't bother
+                        // calling invalidateAll either way 
+                        // (esp calling invalidateAll(Iterable) will
+                        // potentially iterate over all keys even though
+                        // there's nothing to be deleted)
+                    } else {
+                        // however, if the docChildrenCache is not empty,
+                        // use the invalidateAll(Iterable) variant,
+                        // passing it a Iterable<StringValue>, as that's
+                        // what is contained in the cache
+                        docChildrenCache.invalidateAll(asStringValueIterable(externalSort));
+                        long newSize = docChildrenCache.size();
+                        LOG.trace("backgroundRead: docChildrenCache invalidation result: orig: {}, new: {} ", origSize, newSize);
+                    }
+                } catch (Exception ioe) {
+                    LOG.error("backgroundRead: got IOException during external sorting/cache invalidation (as a result, invalidating entire cache): "+ioe, ioe);
+                    stats.cacheStats = store.invalidateCache();
+                    docChildrenCache.invalidateAll();
+                }
+            }
             stats.cacheInvalidationTime = clock.getTime() - time;
             time = clock.getTime();
 
@@ -1852,6 +1880,34 @@ public final class DocumentNodeStore
         return stats;
     }
 
+    private Iterable<StringValue> asStringValueIterable(final StringSort extSort) {
+        return new Iterable<StringValue>() {
+
+            @Override
+            public Iterator<StringValue> iterator() {
+                final Iterator<String> delegate = extSort.getIterableIds().iterator();
+                return new Iterator<StringValue>() {
+
+                    @Override
+                    public boolean hasNext() {
+                        return delegate.hasNext();
+                    }
+
+                    @Override
+                    public StringValue next() {
+                        return new StringValue(delegate.next());
+                    }
+
+                    @Override
+                    public void remove() {
+                        delegate.remove();
+                    }
+
+                };
+            }
+        };
+    }
+
     private static class BackgroundReadStats {
         CacheInvalidationStats cacheStats;
         long readHead;
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentStore.java
index 639782c..5ec1d6d 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentStore.java
@@ -233,6 +233,13 @@ public interface DocumentStore {
     CacheInvalidationStats invalidateCache();
 
     /**
+     * Invalidate the document cache but only with entries that match one
+     * of the paths provided.
+     */
+    @CheckForNull
+    CacheInvalidationStats invalidateCache(Iterable<String> paths);
+
+    /**
      * Invalidate the document cache for the given key.
      *
      * @param <T> the document type
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
index 3a5f6e6..2a5a7e7 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
@@ -94,7 +94,6 @@ public final class JournalEntry extends Document {
                         @Nonnull Revision from,
                         @Nonnull Revision to) throws IOException {
         LOG.debug("applyTo: starting for {} to {}", from, to);
-        externalSort.sort();
         // note that it is not de-duplicated yet
         LOG.debug("applyTo: sorting done.");
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/memory/MemoryDocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/memory/MemoryDocumentStore.java
index bfa78d8..3b6c8cc 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/memory/MemoryDocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/memory/MemoryDocumentStore.java
@@ -338,6 +338,11 @@ public class MemoryDocumentStore implements DocumentStore {
     }
 
     @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        return null;
+    }
+    
+    @Override
     public void dispose() {
         // ignore
     }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
index 3f44062..9c2f095 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java
@@ -67,6 +67,7 @@ import org.apache.jackrabbit.oak.plugins.document.cache.CacheInvalidationStats;
 import org.apache.jackrabbit.oak.plugins.document.cache.ForwardingListener;
 import org.apache.jackrabbit.oak.plugins.document.cache.NodeDocOffHeapCache;
 import org.apache.jackrabbit.oak.plugins.document.cache.OffHeapCache;
+import org.apache.jackrabbit.oak.plugins.document.mongo.CacheInvalidator.InvalidationResult;
 import org.apache.jackrabbit.oak.plugins.document.util.StringValue;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.stats.Clock;
@@ -287,6 +288,61 @@ public class MongoDocumentStore implements DocumentStore {
         //that would lead to lesser number of queries
         return CacheInvalidator.createHierarchicalInvalidator(this).invalidateCache();
     }
+    
+    @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        LOG.debug("invalidateCache: start");
+        final InvalidationResult result = new InvalidationResult();
+        int size  = 0;
+
+        final Iterator<String> it = paths.iterator();
+        while(it.hasNext()) {
+            // read chunks of documents only
+            final List<String> ids = new ArrayList<String>(IN_CLAUSE_BATCH_SIZE);
+            while(it.hasNext() && ids.size()<IN_CLAUSE_BATCH_SIZE) {
+                final String path = it.next();
+                final String id = Utils.getIdFromPath(path);
+                if (getCachedNodeDoc(id)!=null) {
+                    // only add those that we actually do have cached
+                    ids.add(id);
+                }
+            }
+            size += ids.size();
+            if (LOG.isTraceEnabled()) {
+                LOG.trace("invalidateCache: batch size: "+ids.size()+" of total so far "+size);
+            }
+            
+            QueryBuilder query = QueryBuilder.start(Document.ID)
+                    .in(ids);
+            // Fetch only the lastRev map and id
+            final BasicDBObject keys = new BasicDBObject(Document.ID, 1);
+            keys.put(Document.MOD_COUNT, 1);
+            
+            // Fetch lastRev for each such node
+            DBCursor cursor = nodes.find(query.get(), keys);
+            cursor.setReadPreference(ReadPreference.primary());
+            result.queryCount++;
+            
+            for (DBObject obj : cursor) {
+                result.cacheEntriesProcessedCount++;
+                String id = (String) obj.get(Document.ID);
+                Number modCount = (Number) obj.get(Document.MOD_COUNT);
+                
+                CachedNodeDocument cachedDoc = getCachedNodeDoc(id);
+                if (cachedDoc != null
+                        && !Objects.equal(cachedDoc.getModCount(), modCount)) {
+                    invalidateCache(Collection.NODES, id);
+                    result.invalidationCount++;
+                } else {
+                    result.upToDateCount++;
+                }
+            }
+        }
+
+        result.cacheSize = size;
+        LOG.trace("invalidateCache: end. total: {}",size);
+        return result;
+    }
 
     @Override
     public <T extends Document> void invalidateCache(Collection<T> collection, String key) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/rdb/RDBDocumentStore.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/rdb/RDBDocumentStore.java
index 3280c0a..38a8fa9 100755
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/rdb/RDBDocumentStore.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/rdb/RDBDocumentStore.java
@@ -282,6 +282,12 @@ public class RDBDocumentStore implements DocumentStore {
         }
         return null;
     }
+    
+    @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        //TODO: optimize me
+        return invalidateCache();
+    }
 
     @Override
     public <T extends Document> void invalidateCache(Collection<T> collection, String id) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/LoggingDocumentStoreWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/LoggingDocumentStoreWrapper.java
index 06b87a0..2572750 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/LoggingDocumentStoreWrapper.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/LoggingDocumentStoreWrapper.java
@@ -251,6 +251,17 @@ public class LoggingDocumentStoreWrapper implements DocumentStore {
             throw convert(e);
         }
     }
+    
+    @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        try {
+            logMethod("invalidateCache", paths);
+            return store.invalidateCache(paths);
+        } catch (Exception e) {
+            logException(e);
+            throw convert(e);
+        }
+    }
 
     @Override
     public <T extends Document> void invalidateCache(Collection<T> collection, String key) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/SynchronizingDocumentStoreWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/SynchronizingDocumentStoreWrapper.java
index 1e3be7f..7271391 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/SynchronizingDocumentStoreWrapper.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/SynchronizingDocumentStoreWrapper.java
@@ -107,6 +107,11 @@ public class SynchronizingDocumentStoreWrapper implements DocumentStore {
     }
 
     @Override
+    public synchronized CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        return store.invalidateCache(paths);
+    }
+    
+    @Override
     public synchronized <T extends Document> void invalidateCache(Collection<T> collection, String key) {
         store.invalidateCache(collection, key);
     }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/TimingDocumentStoreWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/TimingDocumentStoreWrapper.java
index 940940a..95e4d8f 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/TimingDocumentStoreWrapper.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/TimingDocumentStoreWrapper.java
@@ -282,6 +282,18 @@ public class TimingDocumentStoreWrapper implements DocumentStore {
             throw convert(e);
         }
     }
+    
+    @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        try {
+            long start = now();
+            CacheInvalidationStats result = base.invalidateCache(paths);
+            updateAndLogTimes("invalidateCache3", start, 0, 0);
+            return result;
+        } catch (Exception e) {
+            throw convert(e);
+        }
+    }
 
     @Override
     public <T extends Document> void invalidateCache(Collection<T> collection, String key) {
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/CountingDocumentStore.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/CountingDocumentStore.java
index 379afd6..33cfb83 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/CountingDocumentStore.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/CountingDocumentStore.java
@@ -183,6 +183,11 @@ public class CountingDocumentStore implements DocumentStore {
     }
 
     @Override
+    public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+        return delegate.invalidateCache(paths);
+    }
+
+    @Override
     public <T extends Document> void invalidateCache(Collection<T> collection,
                                                      String key) {
         delegate.invalidateCache(collection, key);
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
index 685eb4a..4c5de0d 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreTest.java
@@ -60,6 +60,7 @@ import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Type;
@@ -103,8 +104,8 @@ public class DocumentNodeStoreTest {
         DocumentStore docStore = new MemoryDocumentStore();
         DocumentStore testStore = new TimingDocumentStoreWrapper(docStore) {
             @Override
-            public CacheInvalidationStats invalidateCache() {
-                super.invalidateCache();
+            public CacheInvalidationStats invalidateCache(Iterable<String> paths) {
+                super.invalidateCache(paths);
                 semaphore.acquireUninterruptibly();
                 semaphore.release();
                 return null;
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalTest.java
index 70887f6..68bfa92 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalTest.java
@@ -24,13 +24,18 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.cache.CacheStats;
 import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.spi.blob.BlobStore;
@@ -45,6 +50,8 @@ import org.apache.jackrabbit.oak.stats.Clock;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
@@ -54,6 +61,8 @@ public class JournalTest {
 
     private static final boolean MONGO_DB = false;
 //    private static final boolean MONGO_DB = true;
+
+    private static final Logger LOG = LoggerFactory.getLogger(JournalTest.class);
     
     private TestBuilder builder;
 
@@ -62,6 +71,8 @@ public class JournalTest {
 
     private List<DocumentMK> mks = Lists.newArrayList();
 
+    private Random random;
+
     class DiffingObserver implements Observer, Runnable, NodeStateDiff {
 
         final List<DocumentNodeState> incomingRootStates1 = Lists.newArrayList();
@@ -217,6 +228,205 @@ public class JournalTest {
         
     }
     
+    @Before
+    public void setup() {
+        random = new Random();
+    }
+    
+    @Test
+    public void cacheInvalidationTest() throws Exception {
+        final DocumentNodeStore ns1 = createMK(1, 0).getNodeStore();
+        final DocumentNodeStore ns2 = createMK(2, 0).getNodeStore();
+        LOG.info("cache size 1: "+(ns1.getDocumentStore().getCacheStats()==null ? "null" : ns1.getDocumentStore().getCacheStats().getElementCount()));
+        
+        // invalidate both caches under test first
+        ns1.invalidateDocChildrenCache();
+        ns1.getDocumentStore().invalidateCache();
+
+        {
+            DocumentStore s = ns1.getDocumentStore();
+            CacheStats cacheStats = s.getCacheStats();
+            LOG.info("m.size="+(cacheStats==null ? "null" : cacheStats.getElementCount()));
+        }
+        LOG.info("cache size 2: "+(ns1.getDocumentStore().getCacheStats()==null ? "null" : ns1.getDocumentStore().getCacheStats().getElementCount()));
+        
+        // first create child node in instance 1
+        final List<String> paths = createRandomPaths(1, 5000000, 1000);
+        int i=0;
+        for(String path : paths) {
+            if (i++%100==0) {
+                LOG.info("at "+i);
+            }
+            getOrCreate(ns1, path, false);
+        }
+        final List<String> paths2 = createRandomPaths(20, 2345, 100);
+        getOrCreate(ns1, paths2, false);
+        ns1.runBackgroundOperations();
+        for(String path : paths) {
+            assertDocCache(ns1, true, path);
+        }
+        
+        {
+            DocumentStore s = ns1.getDocumentStore();
+            CacheStats cacheStats = s.getCacheStats();
+            LOG.info("m.size="+(cacheStats==null ? "null" : cacheStats.getElementCount()));
+        }
+        
+        LOG.info("cache size 2: "+(ns1.getDocumentStore().getCacheStats()==null ? "null" : ns1.getDocumentStore().getCacheStats().getElementCount()));
+        long time = System.currentTimeMillis();
+        for(int j=0; j<100; j++) {
+            long now = System.currentTimeMillis();
+            LOG.info("loop "+j+", "+(now-time)+"ms");
+            time = now;
+            final Set<String> electedPaths = choose(paths2, random.nextInt(30));
+            {
+                // choose a random few from above created paths and modify them
+                final long t1 = System.currentTimeMillis();
+                ns2.runBackgroundOperations(); // make sure ns2 has the latest from ns1
+                final long t2 = System.currentTimeMillis();
+                LOG.info("ns2 background took "+(t2-t1)+"ms");
+    
+                for(String electedPath : electedPaths) {
+                    // modify /child in another instance 2
+                    setProperty(ns2, electedPath, "p", "ns2"+System.currentTimeMillis(), false);
+                }
+                final long t3 = System.currentTimeMillis();
+                LOG.info("setting props "+(t3-t2)+"ms");
+                
+                ns2.runBackgroundOperations();
+                final long t4 = System.currentTimeMillis();
+                LOG.info("ns2 background took2 "+(t4-t3)+"ms");
+            }
+    
+            // that should not have changed the fact that we have it cached in 'ns1'
+            for(String electedPath : electedPaths) {
+                assertDocCache(ns1, true, electedPath);
+            }
+            
+            // doing a backgroundOp now should trigger invalidation
+            // which thx to the external modification will remove the entry from the cache:
+            ns1.runBackgroundOperations();
+            if (MONGO_DB) {
+                // applies only to MONGO_DB however!
+                for(String electedPath : electedPaths) {
+                    assertDocCache(ns1, false, electedPath);
+                }
+            }
+    
+            // when I access it again with 'ns1', then it gets cached again:
+            for(String electedPath : electedPaths) {
+                getOrCreate(ns1, electedPath, false);
+                assertDocCache(ns1, true, electedPath);
+            }
+        }
+    }
+
+    private Set<String> choose(List<String> paths, int howMany) {
+        final Set<String> result = new HashSet<String>();
+        while(result.size()<howMany) {
+            result.add(paths.get(random.nextInt(paths.size())));
+        }
+        return result;
+    }
+
+    private List<String> createRandomPaths(int depth, int avgChildrenPerLevel, int num) {
+        final Set<String> result = new HashSet<String>();
+        while(result.size()<num) {
+            result.add(createRandomPath(depth, avgChildrenPerLevel));
+        }
+        return new ArrayList<String>(result);
+    }
+
+    private String createRandomPath(int depth, int avgChildrenPerLevel) {
+        StringBuffer sb = new StringBuffer();
+        for(int i=0; i<depth; i++) {
+            sb.append("/");
+            sb.append("r"+random.nextInt(avgChildrenPerLevel));
+        }
+        return sb.toString();
+    }
+
+    @Test
+    public void simpleCacheInvalidationTest() throws Exception {
+        final DocumentNodeStore ns1 = createMK(1, 0).getNodeStore();
+        final DocumentNodeStore ns2 = createMK(2, 0).getNodeStore();
+        
+        // invalidate both caches under test first
+        ns1.invalidateDocChildrenCache();
+        ns1.getDocumentStore().invalidateCache();
+        
+        // first create child node in instance 1
+        getOrCreate(ns1, "/child", true);
+        assertDocCache(ns1, true, "/child");
+
+        {
+            // modify /child in another instance 2
+            ns2.runBackgroundOperations(); // read latest changes from ns1
+            setProperty(ns2, "/child", "p", "ns2"+System.currentTimeMillis(), true);
+        }
+        // that should not have changed the fact that we have it cached in 'ns'
+        assertDocCache(ns1, true, "/child");
+
+        // doing a backgroundOp now should trigger invalidation
+        // which thx to the external modification will remove the entry from the cache:
+        ns1.runBackgroundOperations();
+        assertDocCache(ns1, false, "/child");
+
+        // when I access it again with 'ns', then it gets cached again:
+        getOrCreate(ns1, "/child", false);
+        assertDocCache(ns1, true, "/child");
+    }
+
+    private void assertDocCache(DocumentNodeStore ns, boolean expected, String path) {
+        String id = Utils.getIdFromPath(path);
+        boolean exists = ns.getDocumentStore().getIfCached(Collection.NODES, id)!=null;
+        if (exists!=expected) {
+            if (expected) {
+                fail("assertDocCache: did not find in cache even though expected: "+path);
+            } else {
+                fail("assertDocCache: found in cache even though not expected: "+path);
+            }
+        }
+    }
+
+    private void setProperty(DocumentNodeStore ns, String path, String key, String value, boolean runBgOpsAfterCreation) throws CommitFailedException {
+        NodeBuilder rootBuilder = ns.getRoot().builder();
+        doGetOrCreate(rootBuilder, path).setProperty(key, value);
+        ns.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        if (runBgOpsAfterCreation) {
+            ns.runBackgroundOperations();
+        }
+    }
+
+    
+    private void getOrCreate(DocumentNodeStore ns, List<String> paths, boolean runBgOpsAfterCreation) throws CommitFailedException {
+        NodeBuilder rootBuilder = ns.getRoot().builder();
+        for(String path:paths) {
+            doGetOrCreate(rootBuilder, path);
+        }
+        ns.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        if (runBgOpsAfterCreation) {
+            ns.runBackgroundOperations();
+        }
+    }
+
+    private void getOrCreate(DocumentNodeStore ns, String path, boolean runBgOpsAfterCreation) throws CommitFailedException {
+        NodeBuilder rootBuilder = ns.getRoot().builder();
+        doGetOrCreate(rootBuilder, path);
+        ns.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        if (runBgOpsAfterCreation) {
+            ns.runBackgroundOperations();
+        }
+    }
+
+    private NodeBuilder doGetOrCreate(NodeBuilder builder, String path) {
+        String[] parts = path.split("/");
+        for(int i=1; i<parts.length; i++) {
+            builder = builder.child(parts[i]);
+        }
+        return builder;
+    }
+
     @Test
     public void cleanupTest() throws Exception {
         DocumentMK mk1 = createMK(0 /* clusterId: 0 => uses clusterNodes collection */, 0);
