diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
index cded605..063727e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/Commit.java
@@ -31,11 +31,11 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-
 import org.apache.jackrabbit.oak.api.PropertyState;
-import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -76,6 +76,7 @@
 
     /** Set of all nodes which have binary properties. **/
     private HashSet<String> nodesWithBinaries = Sets.newHashSet();
+    private HashMap<String, String> bundledNodes = Maps.newHashMap();
 
     /**
      * Create a new Commit.
@@ -143,6 +144,10 @@ void updateProperty(String path, String propertyName, String value) {
         op.setMapEntry(key, revision, value);
     }
 
+    void addBundledNode(String path, String bundlingRootPath) {
+        bundledNodes.put(path, bundlingRootPath);
+    }
+
     void markNodeHavingBinary(String path) {
         this.nodesWithBinaries.add(path);
     }
@@ -375,6 +380,11 @@ private void updateParentChildStatus() {
                 continue;
             }
 
+            //Ignore setting children path for bundled nodes
+            if (isBundled(parentPath)){
+                continue;
+            }
+
             processedParents.add(parentPath);
             UpdateOp op = getUpdateOperationForNode(parentPath);
             NodeDocument.setChildrenFlag(op, true);
@@ -648,10 +658,21 @@ public void applyToCache(RevisionVector before, boolean isBranchCommit) {
                 }
             }
             UpdateOp op = operations.get(path);
+
+            //In case its bundled the op would be the one for
+            //bundling root
+            boolean bundled = isBundled(path);
+            if (bundled){
+                String bundlingRoot = bundledNodes.get(path);
+                op = operations.get(bundlingRoot);
+            }
+
             boolean isNew = op != null && op.isNew();
             if (op == null || !hasContentChanges(op) || denotesRoot(path)) {
                 // track intermediate node and root
-                tracker.track(path);
+                if (!bundled) {
+                    tracker.track(path);
+                }
             }
             nodeStore.applyChanges(before, after, rev, path, isNew,
                     added, removed, changed, cacheEntry);
@@ -684,6 +705,10 @@ public void removeNode(String path, NodeState state) {
         }
     }
 
+    private boolean isBundled(String path) {
+        return bundledNodes.containsKey(path);
+    }
+
     private static final Function<UpdateOp.Key, String> KEY_TO_NAME =
             new Function<UpdateOp.Key, String>() {
         @Override
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/CommitDiff.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/CommitDiff.java
index cb09bdd..447c6fd 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/CommitDiff.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/CommitDiff.java
@@ -21,15 +21,17 @@
 import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Type;
-import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.json.BlobSerializer;
 import org.apache.jackrabbit.oak.json.JsonSerializer;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingHandler;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor;
 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 org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
 import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.MISSING_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
 
 /**
  * Implementation of a {@link NodeStateDiff}, which translates the diffs into
@@ -41,25 +43,26 @@
 
     private final Commit commit;
 
-    private final String path;
-
     private final JsopBuilder builder;
 
     private final BlobSerializer blobs;
 
+    private final BundlingHandler bundlingHandler;
+
     CommitDiff(@Nonnull DocumentNodeStore store, @Nonnull Commit commit,
                @Nonnull BlobSerializer blobs) {
-        this(checkNotNull(store), checkNotNull(commit), "/",
+        this(checkNotNull(store), checkNotNull(commit), store.getBundlingConfigHandler().newBundlingHandler(),
                 new JsopBuilder(), checkNotNull(blobs));
     }
 
-    private CommitDiff(DocumentNodeStore store, Commit commit, String path,
+    private CommitDiff(DocumentNodeStore store, Commit commit, BundlingHandler bundlingHandler,
                JsopBuilder builder, BlobSerializer blobs) {
         this.store = store;
         this.commit = commit;
-        this.path = path;
+        this.bundlingHandler = bundlingHandler;
         this.builder = builder;
         this.blobs = blobs;
+        performBundlingRelatedOperations();
     }
 
     @Override
@@ -76,46 +79,103 @@ public boolean propertyChanged(PropertyState before, PropertyState after) {
 
     @Override
     public boolean propertyDeleted(PropertyState before) {
-        commit.updateProperty(path, before.getName(), null);
+        commit.updateProperty(bundlingHandler.getRootBundlePath(), bundlingHandler.getPropertyPath(before.getName()), null);
         return true;
     }
 
     @Override
     public boolean childNodeAdded(String name, NodeState after) {
-        String p = PathUtils.concat(path, name);
-        commit.addNode(new DocumentNodeState(store, p,
-                new RevisionVector(commit.getRevision())));
+        BundlingHandler child = bundlingHandler.childAdded(name, after);
+        if (child.isBundlingRoot()) {
+            commit.addNode(new DocumentNodeState(store, child.getRootBundlePath(),
+                    new RevisionVector(commit.getRevision())));
+        }
+        setChildrenFlagOnAdd(child);
         return after.compareAgainstBaseState(EMPTY_NODE,
-                new CommitDiff(store, commit, p, builder, blobs));
+                new CommitDiff(store, commit, child, builder, blobs));
     }
 
     @Override
     public boolean childNodeChanged(String name,
                                     NodeState before,
                                     NodeState after) {
-        String p = PathUtils.concat(path, name);
+        //TODO [bundling] Handle change of primaryType. Current approach would work
+        //but if bundling was enabled for previous nodetype its "side effect"
+        //would still impact even though new nodetype does not have bundling enabled
+        BundlingHandler child = bundlingHandler.childChanged(name, after);
         return after.compareAgainstBaseState(before,
-                new CommitDiff(store, commit, p, builder, blobs));
+                new CommitDiff(store, commit, child, builder, blobs));
     }
 
     @Override
     public boolean childNodeDeleted(String name, NodeState before) {
-        String p = PathUtils.concat(path, name);
-        commit.removeNode(p, before);
+        BundlingHandler child = bundlingHandler.childDeleted(name, before);
+        if (child.isBundlingRoot()) {
+            commit.removeNode(child.getRootBundlePath(), before);
+        }
         return MISSING_NODE.compareAgainstBaseState(before,
-                new CommitDiff(store, commit, p, builder, blobs));
+                new CommitDiff(store, commit, child, builder, blobs));
     }
 
     //----------------------------< internal >----------------------------------
 
+    private void performBundlingRelatedOperations() {
+        setMetaProperties();
+        informCommitAboutBundledNodes();
+        removeRemovedProps();
+    }
+
+    private void setMetaProperties() {
+        for (PropertyState ps : bundlingHandler.getMetaProps()){
+            setProperty(ps);
+        }
+    }
+
+    private void informCommitAboutBundledNodes() {
+        if (bundlingHandler.isBundledNode()){
+            commit.addBundledNode(bundlingHandler.getNodeFullPath(), bundlingHandler.getRootBundlePath());
+        }
+    }
+
+    private void removeRemovedProps() {
+        for (String propName : bundlingHandler.getRemovedProps()){
+            commit.updateProperty(bundlingHandler.getRootBundlePath(),
+                    bundlingHandler.getPropertyPath(propName), null);
+        }
+    }
+
+    private void setChildrenFlagOnAdd(BundlingHandler child) {
+        //Add hasChildren marker for bundling case
+        String propName = null;
+        if (child.isBundledNode()){
+            //1. Child is a bundled node. In that case current node would be part
+            //   same NodeDocument in which the child would be saved
+            propName = DocumentBundlor.META_PROP_BUNDLED_CHILD;
+        } else if (bundlingHandler.isBundledNode()){
+            //2. Child is a non bundled node but current node was bundled. This would
+            //   be the case where child node is not covered by bundling pattern. In
+            //   that case also add marker to current node
+            //   For case when current node is bundled  but is bundling root
+            //   this info is already captured in _hasChildren flag
+            propName = DocumentBundlor.META_PROP_NON_BUNDLED_CHILD;
+        }
+
+        //Retouch the property if already present to enable
+        //hierarchy conflict detection
+        if (propName != null){
+            setProperty(createProperty(propName, Boolean.TRUE));
+        }
+    }
+
     private void setProperty(PropertyState property) {
         builder.resetWriter();
         JsonSerializer serializer = new JsonSerializer(builder, blobs);
         serializer.serialize(property);
-        commit.updateProperty(path, property.getName(), serializer.toString());
+        commit.updateProperty(bundlingHandler.getRootBundlePath(), bundlingHandler.getPropertyPath(property.getName()),
+                 serializer.toString());
         if ((property.getType() == Type.BINARY)
                 || (property.getType() == Type.BINARIES)) {
-            this.commit.markNodeHavingBinary(this.path);
+            this.commit.markNodeHavingBinary(bundlingHandler.getRootBundlePath());
         }
     }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java
index f25fe68..aaa438e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentMK.java
@@ -582,6 +582,7 @@ private static void append(DocumentNodeState node,
         private DocumentNodeStoreStatsCollector nodeStoreStatsCollector;
         private Map<CacheType, PersistentCacheStats> persistentCacheStats =
                 new EnumMap<CacheType, PersistentCacheStats>(CacheType.class);
+        private boolean bundlingDisabled;
 
         public Builder() {
         }
@@ -1027,6 +1028,15 @@ public boolean isDisableBranches() {
             return disableBranches;
         }
 
+        public Builder setBundlingDisabled(boolean enabled) {
+            bundlingDisabled = enabled;
+            return this;
+        }
+
+        public boolean isBundlingDisabled() {
+            return bundlingDisabled;
+        }
+
         VersionGCSupport createVersionGCSupport() {
             DocumentStore store = getDocumentStore();
             if (store instanceof MongoDocumentStore) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
index f69e950..3dc6feb 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeState.java
@@ -41,6 +41,9 @@
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.commons.json.JsopWriter;
 import org.apache.jackrabbit.oak.json.JsonSerializer;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlorUtils;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.Matcher;
 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;
@@ -54,6 +57,7 @@
 import com.google.common.collect.Iterators;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
 import static org.apache.jackrabbit.oak.commons.StringUtils.estimateMemoryUsage;
 
 /**
@@ -81,6 +85,7 @@
     private final boolean hasChildren;
 
     private final DocumentNodeStore store;
+    private final BundlingContext bundlingContext;
 
     private AbstractDocumentNodeState cachedSecondaryState;
 
@@ -106,13 +111,24 @@ private DocumentNodeState(@Nonnull DocumentNodeStore store,
                               boolean hasChildren,
                               @Nullable RevisionVector lastRevision,
                               boolean fromExternalChange) {
+        this(store, path, lastRevision, rootRevision,
+                fromExternalChange, createBundlingContext(checkNotNull(properties), hasChildren));
+    }
+
+    private DocumentNodeState(@Nonnull DocumentNodeStore store,
+                              @Nonnull String path,
+                              @Nullable RevisionVector lastRevision,
+                              @Nullable RevisionVector rootRevision,
+                              boolean fromExternalChange,
+                              BundlingContext bundlingContext) {
         this.store = checkNotNull(store);
         this.path = checkNotNull(path);
         this.rootRevision = checkNotNull(rootRevision);
         this.lastRevision = lastRevision;
         this.fromExternalChange = fromExternalChange;
-        this.hasChildren = hasChildren;
-        this.properties = checkNotNull(properties);
+        this.properties = bundlingContext.getProperties();
+        this.bundlingContext = bundlingContext;
+        this.hasChildren = bundlingContext.hasChildren();
     }
 
     /**
@@ -134,8 +150,7 @@ public DocumentNodeState withRootRevision(@Nonnull RevisionVector root,
         if (rootRevision.equals(root) && fromExternalChange == externalChange) {
             return this;
         } else {
-            return new DocumentNodeState(store, path, root, properties,
-                    hasChildren, lastRevision, externalChange);
+            return new DocumentNodeState(store, path, lastRevision, root, externalChange, bundlingContext);
         }
     }
 
@@ -144,9 +159,8 @@ public DocumentNodeState withRootRevision(@Nonnull RevisionVector root,
      *          {@link #fromExternalChange} flag set to {@code true}.
      */
     @Nonnull
-    DocumentNodeState fromExternalChange() {
-        return new DocumentNodeState(store, path, rootRevision, properties, hasChildren,
-                lastRevision, true);
+    public DocumentNodeState fromExternalChange() {
+        return new DocumentNodeState(store, path, lastRevision, rootRevision, true, bundlingContext);
     }
 
     /**
@@ -237,16 +251,22 @@ public long getChildNodeCount(long max) {
         if (!hasChildren) {
             return 0;
         }
+
+        int bundledChildCount = bundlingContext.getBundledChildNodeNames().size();
+        if (bundlingContext.hasOnlyBundledChildren()){
+            return bundledChildCount;
+        }
+
         if (max > DocumentNodeStore.NUM_CHILDREN_CACHE_LIMIT) {
             // count all
-            return Iterators.size(new ChildNodeEntryIterator());
+            return Iterables.size(getChildNodeEntries());
         }
         Children c = store.getChildren(this, null, (int) max);
         if (c.hasMore) {
             return Long.MAX_VALUE;
         } else {
             // we know the exact value
-            return c.children.size();
+            return c.children.size() + bundledChildCount;
         }
     }
 
@@ -270,7 +290,11 @@ public long getPropertyCount() {
         return new Iterable<ChildNodeEntry>() {
             @Override
             public Iterator<ChildNodeEntry> iterator() {
-                return new ChildNodeEntryIterator();
+                //If all the children are bundled
+                if (bundlingContext.hasOnlyBundledChildren()){
+                    return getBundledChildren();
+                }
+                return Iterators.concat(getBundledChildren(), new ChildNodeEntryIterator());
             }
         };
     }
@@ -308,7 +332,10 @@ public NodeBuilder builder() {
     }
 
     String getPropertyAsString(String propertyName) {
-        PropertyState prop = properties.get(propertyName);
+        return asString(properties.get(propertyName));
+    }
+
+    private String asString(PropertyState prop) {
         if (prop == null) {
             return null;
         } else if (prop instanceof DocumentPropertyState) {
@@ -414,7 +441,17 @@ private AbstractDocumentNodeState getChildNodeDoc(String childNodeName){
             }
             return null;
         }
-        return store.getNode(PathUtils.concat(getPath(), childNodeName), lastRevision);
+
+        Matcher child = bundlingContext.matcher.next(childNodeName);
+        if (child.isMatch()){
+            if (bundlingContext.hasChildNode(child.getMatchedPath())){
+                return createBundledState(childNodeName, child);
+            } else {
+                return null;
+            }
+        }
+
+        return store.getNode(concat(getPath(), childNodeName), lastRevision);
     }
 
     @CheckForNull
@@ -480,8 +517,8 @@ public String asString() {
         }
         if (properties.size() > 0) {
             json.key("prop").object();
-            for (String k : properties.keySet()) {
-                json.key(k).value(getPropertyAsString(k));
+            for (Map.Entry<String, PropertyState> e : bundlingContext.getAllProperties().entrySet()) {
+                json.key(e.getKey()).value(asString(e.getValue()));
             }
             json.endObject();
         }
@@ -668,4 +705,120 @@ private void fetchMore() {
         }
     }
 
+    //~----------------------------------------------< Bundling >
+
+    private AbstractDocumentNodeState createBundledState(String childNodeName, Matcher child) {
+        return new DocumentNodeState(
+                store,
+                concat(path, childNodeName),
+                lastRevision,
+                rootRevision,
+                fromExternalChange,
+                bundlingContext.childContext(child));
+    }
+
+    private Iterator<ChildNodeEntry> getBundledChildren(){
+        return Iterators.transform(bundlingContext.getBundledChildNodeNames().iterator(),
+                new Function<String, ChildNodeEntry>() {
+            @Override
+            public ChildNodeEntry apply(final String childNodeName) {
+                return new AbstractChildNodeEntry() {
+                    @Nonnull
+                    @Override
+                    public String getName() {
+                        return childNodeName;
+                    }
+
+                    @Nonnull
+                    @Override
+                    public NodeState getNodeState() {
+                        return createBundledState(childNodeName, bundlingContext.matcher.next(childNodeName));
+                    }
+                };
+            }
+        });
+    }
+
+    private static BundlingContext createBundlingContext(Map<String, PropertyState> properties,
+                                                         boolean hasNonBundledChildren) {
+        PropertyState bundlorConfig = properties.get(DocumentBundlor.META_PROP_PATTERN);
+        Matcher matcher = Matcher.NON_MATCHING;
+        boolean hasBundledChildren = false;
+        if (bundlorConfig != null){
+            matcher = DocumentBundlor.from(bundlorConfig).createMatcher();
+            hasBundledChildren = hasBundledProperty(properties, matcher, DocumentBundlor.META_PROP_BUNDLED_CHILD);
+        }
+        return new BundlingContext(matcher, properties, hasBundledChildren, hasNonBundledChildren);
+    }
+
+    private static boolean hasBundledProperty(Map<String, PropertyState> props, Matcher matcher, String propName){
+        String key = concat(matcher.getMatchedPath(), propName);
+        return props.containsKey(key);
+    }
+
+    private static class BundlingContext {
+        final Matcher matcher;
+        final Map<String, PropertyState> rootProperties;
+        final boolean hasBundledChildren;
+        final boolean hasNonBundledChildren;
+
+        public BundlingContext(Matcher matcher, Map<String, PropertyState> rootProperties,
+                               boolean hasBundledChildren, boolean hasNonBundledChildren) {
+            this.matcher = matcher;
+            this.rootProperties = ImmutableMap.copyOf(rootProperties);
+            this.hasBundledChildren = hasBundledChildren;
+            this.hasNonBundledChildren = hasNonBundledChildren;
+        }
+
+        public BundlingContext childContext(Matcher childMatcher){
+            return new BundlingContext(childMatcher, rootProperties,
+                    hasBundledChildren(childMatcher), hasNonBundledChildren(childMatcher));
+        }
+
+        public Map<String, PropertyState> getProperties(){
+            if (matcher.isMatch()){
+                return BundlorUtils.getMatchingProperties(rootProperties, matcher);
+            }
+            return rootProperties;
+        }
+
+        public Map<String, PropertyState> getAllProperties(){
+            return rootProperties;
+        }
+
+        public boolean hasChildNode(String relativePath){
+            String key = concat(relativePath, DocumentBundlor.META_PROP_NODE);
+            return rootProperties.containsKey(key);
+        }
+
+        public boolean hasChildren(){
+            return hasNonBundledChildren || hasBundledChildren;
+        }
+
+        public boolean hasOnlyBundledChildren(){
+            return !hasNonBundledChildren;
+        }
+
+        public List<String> getBundledChildNodeNames(){
+            if (matcher.isMatch()) {
+                return BundlorUtils.getChildNodeNames(rootProperties.keySet(), matcher);
+            }
+            return Collections.emptyList();
+        }
+
+        private boolean hasBundledChildren(Matcher matcher){
+            if (matcher.isMatch()){
+                return hasBundledProperty(rootProperties, matcher, DocumentBundlor.META_PROP_BUNDLED_CHILD);
+            }
+            return false;
+        }
+
+        private boolean hasNonBundledChildren(Matcher matcher){
+            if (matcher.isMatch()){
+                return hasBundledProperty(rootProperties, matcher, DocumentBundlor.META_PROP_NON_BUNDLED_CHILD);
+            }
+            return false;
+        }
+
+    }
 }
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 4623e0e..de4bbed 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
@@ -84,6 +84,7 @@
 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.bundlor.BundlingConfigHandler;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.PersistentCache;
 import org.apache.jackrabbit.oak.plugins.document.persistentCache.broadcast.DynamicBroadcastConfig;
 import org.apache.jackrabbit.oak.plugins.document.util.ReadOnlyDocumentStoreWrapperFactory;
@@ -415,6 +416,8 @@ public boolean apply(@Nullable String input) {
 
     private final StatisticsProvider statisticsProvider;
 
+    private final BundlingConfigHandler bundlingConfigHandler = new BundlingConfigHandler();
+
     public DocumentNodeStore(DocumentMK.Builder builder) {
         this.blobStore = builder.getBlobStore();
         this.statisticsProvider = builder.getStatisticsProvider();
@@ -568,8 +571,13 @@ public int getMemory() {
         this.mbean = createMBean();
         LOG.info("Initialized DocumentNodeStore with clusterNodeId: {} ({})", clusterId,
                 getClusterNodeInfoDisplayString());
+
+        if (!builder.isBundlingDisabled()) {
+            bundlingConfigHandler.initialize(this, executor);
+        }
     }
 
+
     /**
      * Recover _lastRev recovery if needed.
      *
@@ -602,6 +610,13 @@ public void dispose() {
             // only dispose once
             return;
         }
+
+        try {
+            bundlingConfigHandler.close();
+        } catch (IOException e) {
+            LOG.warn("Error closing bundlingConfigHandler", bundlingConfigHandler, e);
+        }
+
         // notify background threads waiting on isDisposed
         synchronized (isDisposed) {
             isDisposed.notifyAll();
@@ -1101,6 +1116,10 @@ DocumentNodeState readNode(String path, RevisionVector readRevision) {
         return result;
     }
 
+    public BundlingConfigHandler getBundlingConfigHandler() {
+        return bundlingConfigHandler;
+    }
+
     /**
      * Apply the changes of a node to the cache.
      *
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
index 0c2035f..56f8831 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java
@@ -80,6 +80,7 @@
 import org.apache.jackrabbit.oak.spi.blob.BlobStoreWrapper;
 import org.apache.jackrabbit.oak.spi.blob.GarbageCollectableBlobStore;
 import org.apache.jackrabbit.oak.spi.blob.stats.BlobStoreStatsMBean;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean;
 import org.apache.jackrabbit.oak.spi.state.Clusterable;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.spi.state.RevisionGC;
@@ -363,6 +364,14 @@ static DocumentStoreType fromString(String type) {
             description = "Type of DocumentStore to use for persistence. Defaults to MONGO"
     )
     public static final String PROP_DS_TYPE = "documentStoreType";
+
+    private static final boolean DEFAULT_BUNDLING_DISABLED = false;
+    @Property(boolValue = DEFAULT_BUNDLING_DISABLED,
+            label = "Bundling Disabled",
+            description = "Boolean value indicating that Node bundling is disabled"
+    )
+    private static final String PROP_BUNDLING_DISABLED = "bundlingDisabled";
+
     private DocumentStoreType documentStoreType;
 
     @Reference
@@ -418,6 +427,7 @@ private void registerNodeStore() throws IOException {
         String journalCache = PropertiesUtil.toString(prop(PROP_JOURNAL_CACHE), DEFAULT_JOURNAL_CACHE);
         int cacheSegmentCount = toInteger(prop(PROP_CACHE_SEGMENT_COUNT), DEFAULT_CACHE_SEGMENT_COUNT);
         int cacheStackMoveDistance = toInteger(prop(PROP_CACHE_STACK_MOVE_DISTANCE), DEFAULT_CACHE_STACK_MOVE_DISTANCE);
+        boolean bundlingDisabled = toBoolean(prop(PROP_BUNDLING_DISABLED), DEFAULT_BUNDLING_DISABLED);
         DocumentMK.Builder mkBuilder =
                 new DocumentMK.Builder().
                 setStatisticsProvider(statisticsProvider).
@@ -429,6 +439,7 @@ private void registerNodeStore() throws IOException {
                         diffCachePercentage).
                 setCacheSegmentCount(cacheSegmentCount).
                 setCacheStackMoveDistance(cacheStackMoveDistance).
+                setBundlingDisabled(bundlingDisabled).
                 setLeaseCheck(true /* OAK-2739: enabled by default */).
                 setLeaseFailureHandler(new LeaseFailureHandler() {
                     
@@ -808,6 +819,14 @@ public void run() {
                     BlobStoreStatsMBean.TYPE,
                     ds.getClass().getSimpleName()));
         }
+
+        if (!mkBuilder.isBundlingDisabled()){
+            registrations.add(registerMBean(whiteboard,
+                    BackgroundObserverMBean.class,
+                    store.getBundlingConfigHandler().getMBean(),
+                    BackgroundObserverMBean.TYPE,
+                    "BundlingConfigObserver"));
+        }
     }
 
     private void registerLastRevRecoveryJob(final DocumentNodeStore nodeStore) {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistry.java
new file mode 100644
index 0000000..970f52d
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistry.java
@@ -0,0 +1,156 @@
+/*
+ * 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.bundlor;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.CheckForNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
+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 static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+
+public class BundledTypesRegistry {
+    public static BundledTypesRegistry NOOP = BundledTypesRegistry.from(EMPTY_NODE);
+    private final Map<String, DocumentBundlor> bundlors;
+
+    public BundledTypesRegistry(Map<String, DocumentBundlor> bundlors) {
+        this.bundlors = ImmutableMap.copyOf(bundlors);
+    }
+
+    public static BundledTypesRegistry from(NodeState configParentState){
+        Map<String, DocumentBundlor> bundlors = Maps.newHashMap();
+        for (ChildNodeEntry e : configParentState.getChildNodeEntries()){
+            bundlors.put(e.getName(), DocumentBundlor.from(e.getNodeState()));
+        }
+        return new BundledTypesRegistry(bundlors);
+    }
+
+    @CheckForNull
+    public DocumentBundlor getBundlor(NodeState state) {
+        //Prefer mixin (as they are more specific) over primaryType
+        for (String mixin : getMixinNames(state)){
+            DocumentBundlor bundlor = bundlors.get(mixin);
+            if (bundlor != null){
+                return bundlor;
+            }
+        }
+        return bundlors.get(getPrimaryTypeName(state));
+    }
+
+    Map<String, DocumentBundlor> getBundlors() {
+        return bundlors;
+    }
+
+    private static String getPrimaryTypeName(NodeState nodeState) {
+        PropertyState ps = nodeState.getProperty(JcrConstants.JCR_PRIMARYTYPE);
+        return (ps == null) ? JcrConstants.NT_BASE : ps.getValue(Type.NAME);
+    }
+
+    private static Iterable<String> getMixinNames(NodeState nodeState) {
+        PropertyState ps = nodeState.getProperty(JcrConstants.JCR_MIXINTYPES);
+        return (ps == null) ? Collections.<String>emptyList() : ps.getValue(Type.NAMES);
+    }
+
+    //~--------------------------------------------< Builder >
+
+    public static BundledTypesRegistryBuilder builder(){
+        return new BundledTypesRegistryBuilder(EMPTY_NODE.builder());
+    }
+
+    public static class BundledTypesRegistryBuilder {
+        private final NodeBuilder builder;
+
+        public BundledTypesRegistryBuilder(NodeBuilder builder) {
+            this.builder = builder;
+        }
+
+        public TypeBuilder forType(String typeName){
+            NodeBuilder child = builder.child(typeName);
+            child.setProperty(JcrConstants.JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, Type.NAME);
+            return new TypeBuilder(this, child);
+        }
+
+        public TypeBuilder forType(String typeName, String ... includes){
+            TypeBuilder typeBuilder = forType(typeName);
+            for (String include : includes){
+                typeBuilder.include(include);
+            }
+            return typeBuilder;
+        }
+
+        public BundledTypesRegistry buildRegistry() {
+            return BundledTypesRegistry.from(builder.getNodeState());
+        }
+
+        public NodeState build(){
+            return builder.getNodeState();
+        }
+
+        public static class TypeBuilder {
+            private final BundledTypesRegistryBuilder parent;
+            private final NodeBuilder typeBuilder;
+            private final Set<String> patterns = Sets.newHashSet();
+
+            private TypeBuilder(BundledTypesRegistryBuilder parent, NodeBuilder typeBuilder) {
+                this.parent = parent;
+                this.typeBuilder = typeBuilder;
+            }
+
+            public TypeBuilder include(String pattern){
+                patterns.add(pattern);
+                return this;
+            }
+
+            public BundledTypesRegistry buildRegistry(){
+                setupPatternProp();
+                return parent.buildRegistry();
+            }
+
+            public BundledTypesRegistryBuilder registry(){
+                setupPatternProp();
+                return parent;
+            }
+
+            public NodeState build(){
+                setupPatternProp();
+                return parent.build();
+            }
+
+            private void setupPatternProp() {
+                typeBuilder.setProperty(createProperty(DocumentBundlor.PROP_PATTERN, patterns, STRINGS));
+            }
+        }
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandler.java
new file mode 100644
index 0000000..7172984
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandler.java
@@ -0,0 +1,123 @@
+/*
+ * 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.bundlor;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserver;
+import org.apache.jackrabbit.oak.spi.commit.BackgroundObserverMBean;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.DefaultEditor;
+import org.apache.jackrabbit.oak.spi.commit.Editor;
+import org.apache.jackrabbit.oak.spi.commit.EditorDiff;
+import org.apache.jackrabbit.oak.spi.commit.Observable;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.commit.SubtreeEditor;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+
+public class BundlingConfigHandler implements Observer, Closeable {
+    public static final String DOCUMENT_NODE_STORE = "documentstore";
+    public static final String BUNDLOR = "bundlor";
+
+    public static final String CONFIG_PATH = "/jcr:system/documentstore/bundlor";
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private NodeState root = EMPTY_NODE;
+    private BackgroundObserver backgroundObserver;
+    private Closeable observerRegistration;
+    private boolean enabled;
+
+    private volatile BundledTypesRegistry registry = BundledTypesRegistry.NOOP;
+
+    private Editor changeDetector = new SubtreeEditor(new DefaultEditor() {
+        @Override
+        public void leave(NodeState before, NodeState after) throws CommitFailedException {
+            recreateRegistry(after);
+        }
+    }, Iterables.toArray(PathUtils.elements(CONFIG_PATH), String.class));
+
+    @Override
+    public synchronized void contentChanged(@Nonnull NodeState root, @Nullable CommitInfo info) {
+        EditorDiff.process(changeDetector, this.root, root);
+        this.root = root;
+    }
+
+    public BundlingHandler newBundlingHandler() {
+        return new BundlingHandler(registry);
+    }
+
+    public void initialize(NodeStore nodeStore, Executor executor) {
+        registerObserver(nodeStore, executor);
+        //If bundling is disabled then initialize would not be invoked
+        //NOOP registry would get used effectively disabling bundling for
+        //new nodes
+        enabled = true;
+        log.info("Bundling of nodes enabled");
+    }
+
+    @Override
+    public void close() throws IOException{
+        if (backgroundObserver != null){
+            observerRegistration.close();
+            backgroundObserver.close();
+        }
+    }
+
+    public BackgroundObserverMBean getMBean(){
+        return checkNotNull(backgroundObserver).getMBean();
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    BundledTypesRegistry getRegistry() {
+        return registry;
+    }
+
+    private void recreateRegistry(NodeState nodeState) {
+        //TODO Any sanity checks
+        registry = BundledTypesRegistry.from(nodeState);
+        log.info("Refreshing the BundledTypesRegistry");
+    }
+
+    private void registerObserver(NodeStore nodeStore, Executor executor) {
+        if (nodeStore instanceof Observable) {
+            backgroundObserver = new BackgroundObserver(this, executor, 5);
+            observerRegistration = ((Observable) nodeStore).addObserver(backgroundObserver);
+        }
+    }
+
+}
+
+
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializer.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializer.java
new file mode 100644
index 0000000..453347f
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializer.java
@@ -0,0 +1,56 @@
+/*
+ * 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.bundlor;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
+import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigHandler.BUNDLOR;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigHandler.DOCUMENT_NODE_STORE;
+
+public enum BundlingConfigInitializer implements RepositoryInitializer {
+    INSTANCE;
+
+    @Override
+    public void initialize(@Nonnull NodeBuilder builder) {
+        if (builder.hasChildNode(JCR_SYSTEM)){
+            NodeBuilder system = builder.getChildNode(JCR_SYSTEM);
+
+            if (!system.hasChildNode(DOCUMENT_NODE_STORE)){
+                NodeBuilder dns = system.child(DOCUMENT_NODE_STORE);
+                dns.setProperty(JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, Type.NAME);
+
+                NodeState registryState = BundledTypesRegistry.builder()
+                        .forType("nt:file", "jcr:content")
+                        .build();
+                NodeBuilder bundlor = dns.setChildNode(BUNDLOR, registryState);
+                bundlor.setProperty(JCR_PRIMARYTYPE, NodeTypeConstants.NT_OAK_UNSTRUCTURED, Type.NAME);
+            }
+        }
+
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandler.java
new file mode 100644
index 0000000..48ef287
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandler.java
@@ -0,0 +1,212 @@
+/*
+ * 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.bundlor;
+
+import java.util.Set;
+
+import com.google.common.collect.Sets;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.commons.PathUtils.ROOT_PATH;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+
+public class BundlingHandler {
+    /**
+     * True property which is used to mark the presence of relative node
+     * This needs to be set when a bundled relative node is added
+     */
+    private static final PropertyState NODE_PRESENCE_MARKER =
+            PropertyStates.createProperty(DocumentBundlor.META_PROP_NODE, Boolean.TRUE);
+    private final BundledTypesRegistry registry;
+    private final String path;
+    private final BundlingContext ctx;
+    private final NodeState nodeState;
+
+    public BundlingHandler(BundledTypesRegistry registry) {
+        this(registry, BundlingContext.NULL, ROOT_PATH, EMPTY_NODE);
+    }
+
+    private BundlingHandler(BundledTypesRegistry registry, BundlingContext ctx, String path, NodeState nodeState) {
+        this.registry = checkNotNull(registry);
+        this.path = path;
+        this.ctx = ctx;
+        this.nodeState = nodeState;
+    }
+
+    /**
+     * Returns property path. For non bundling case this is the actual property name
+     * while for bundling case this is the relative path from bundling root
+     */
+    public String getPropertyPath(String propertyName) {
+        return ctx.isBundling() ? ctx.getPropertyPath(propertyName) : propertyName;
+    }
+
+    /**
+     * Returns true if and only if current node is bundled in another node
+     */
+    public boolean isBundledNode(){
+        return ctx.matcher.depth() > 0;
+    }
+
+    /**
+     * Returns absolute path of the current node
+     */
+    public String getNodeFullPath() {
+        return path;
+    }
+
+    public NodeState getNodeState() {
+        return nodeState;
+    }
+
+    public Set<PropertyState> getMetaProps() {
+        return ctx.metaProps;
+    }
+
+    /**
+     * Returns name of properties which needs to be removed or marked as deleted
+     */
+    public Set<String> getRemovedProps(){
+        return ctx.removedProps;
+    }
+
+    public String getRootBundlePath() {
+        return ctx.isBundling() ? ctx.bundlingPath : path;
+    }
+
+    public BundlingHandler childAdded(String name, NodeState state){
+        String childPath = childPath(name);
+        BundlingContext childContext;
+        Matcher childMatcher = ctx.matcher.next(name);
+        if (childMatcher.isMatch()) {
+            childContext = createChildContext(childMatcher);
+            childContext.addMetaProp(NODE_PRESENCE_MARKER);
+        } else {
+            DocumentBundlor bundlor = registry.getBundlor(state);
+            if (bundlor != null){
+                PropertyState bundlorConfig = bundlor.asPropertyState();
+                childContext = new BundlingContext(childPath, bundlor.createMatcher());
+                childContext.addMetaProp(bundlorConfig);
+            } else {
+                childContext = BundlingContext.NULL;
+            }
+        }
+        return new BundlingHandler(registry, childContext, childPath, state);
+    }
+
+    public BundlingHandler childDeleted(String name, NodeState state){
+        String childPath = childPath(name);
+        BundlingContext childContext;
+        Matcher childMatcher = ctx.matcher.next(name);
+        if (childMatcher.isMatch()) {
+            childContext = createChildContext(childMatcher);
+            removeDeletedChildProperties(state, childContext);
+        } else {
+            childContext = getBundlorContext(childPath, state);
+        }
+        return new BundlingHandler(registry, childContext, childPath, state);
+    }
+
+    public BundlingHandler childChanged(String name, NodeState state){
+        String childPath = childPath(name);
+        BundlingContext childContext;
+        Matcher childMatcher = ctx.matcher.next(name);
+        if (childMatcher.isMatch()) {
+            childContext = createChildContext(childMatcher);
+        } else {
+            childContext = getBundlorContext(childPath, state);
+        }
+
+        return new BundlingHandler(registry, childContext,  childPath, state);
+    }
+
+    public boolean isBundlingRoot() {
+        if (ctx.isBundling()){
+            return ctx.bundlingPath.equals(path);
+        }
+        return true;
+    }
+
+    private String childPath(String name){
+        return PathUtils.concat(path, name);
+    }
+
+    private BundlingContext createChildContext(Matcher childMatcher) {
+        return ctx.child(childMatcher);
+    }
+
+    private static BundlingContext getBundlorContext(String path, NodeState state) {
+        BundlingContext result = BundlingContext.NULL;
+        PropertyState bundlorConfig = state.getProperty(DocumentBundlor.META_PROP_PATTERN);
+        if (bundlorConfig != null){
+            DocumentBundlor bundlor = DocumentBundlor.from(bundlorConfig);
+            result = new BundlingContext(path, bundlor.createMatcher());
+        }
+        return result;
+    }
+
+    private static void removeDeletedChildProperties(NodeState state, BundlingContext childContext) {
+        childContext.removeProperty(DocumentBundlor.META_PROP_NODE);
+        for (PropertyState ps : state.getProperties()){
+            String propName = ps.getName();
+            //In deletion never touch child status related meta props
+            //as they are not to be changed once set
+            if (!propName.startsWith(DocumentBundlor.HAS_CHILD_PROP_PREFIX))
+                childContext.removeProperty(ps.getName());
+        }
+    }
+
+    private static class BundlingContext {
+        static final BundlingContext NULL = new BundlingContext("", Matcher.NON_MATCHING);
+        final String bundlingPath;
+        final Matcher matcher;
+        final Set<PropertyState> metaProps = Sets.newHashSet();
+        final Set<String> removedProps = Sets.newHashSet();
+
+        public BundlingContext(String bundlingPath, Matcher matcher) {
+            this.bundlingPath = bundlingPath;
+            this.matcher = matcher;
+        }
+
+        public BundlingContext child(Matcher matcher){
+            return new BundlingContext(bundlingPath, matcher);
+        }
+
+        public boolean isBundling(){
+            return matcher.isMatch();
+        }
+
+        public String getPropertyPath(String propertyName) {
+            return PathUtils.concat(matcher.getMatchedPath(), propertyName);
+        }
+
+        public void addMetaProp(PropertyState state){
+            metaProps.add(state);
+        }
+
+        public void removeProperty(String name){
+            removedProps.add(name);
+        }
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtils.java
new file mode 100644
index 0000000..714fb7d
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtils.java
@@ -0,0 +1,103 @@
+/*
+ * 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.bundlor;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.META_PROP_NODE;
+
+public final class BundlorUtils {
+
+    public static Map<String, PropertyState> getMatchingProperties(Map<String, PropertyState> props, Matcher matcher){
+        if (!matcher.isMatch()){
+            return Collections.emptyMap();
+        }
+
+        Map<String, PropertyState> result = Maps.newHashMap();
+        for (Map.Entry<String, PropertyState> e : props.entrySet()){
+            String propertyPath = e.getKey();
+
+            //PathUtils.depth include depth for property name. So
+            //reduce 1 to get node depth
+            int depth = PathUtils.getDepth(propertyPath) - 1;
+
+            if (!propertyPath.startsWith(matcher.getMatchedPath())){
+                continue;
+            }
+
+            if (depth != matcher.depth()){
+                continue;
+            }
+
+            if (propertyPath.endsWith(META_PROP_NODE)){
+                continue;
+            }
+
+            //Extract property name from relative property path
+            final String newKey = PathUtils.getName(propertyPath);
+            PropertyState value = e.getValue();
+
+            if (depth > 0){
+                value = new PropertyStateWrapper(value){
+                    @Nonnull
+                    @Override
+                    public String getName() {
+                        return newKey;
+                    }
+                };
+            }
+
+            result.put(newKey, value);
+        }
+        return result;
+    }
+
+    public static List<String> getChildNodeNames(Collection<String> keys, Matcher matcher){
+        List<String> childNodeNames = Lists.newArrayList();
+
+        //Immediate child should have depth 1 more than matcher depth
+        int expectedDepth = matcher.depth() + 1;
+
+        for (String key : keys){
+            List<String> elements = ImmutableList.copyOf(PathUtils.elements(key));
+            int depth = elements.size() - 1;
+
+            if (depth == expectedDepth
+                    && key.startsWith(matcher.getMatchedPath())
+                    && elements.get(elements.size() - 1).equals(META_PROP_NODE)){
+                //Child node name is the second last element
+                //[jcr:content/:self -> [jcr:content, :self]
+                childNodeNames.add(elements.get(elements.size() - 2));
+            }
+        }
+        return childNodeNames;
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcher.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcher.java
new file mode 100644
index 0000000..bbb4bd1
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcher.java
@@ -0,0 +1,91 @@
+/*
+ * 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.bundlor;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+class CompositeMatcher implements Matcher {
+    private final List<Matcher> matchers;
+
+    public static Matcher compose(List<Matcher> matchers){
+        switch (matchers.size()) {
+            case 0:
+                return Matcher.NON_MATCHING;
+            case 1:
+                return matchers.get(0);
+            default:
+                return new CompositeMatcher(matchers);
+        }
+    }
+
+    /**
+     * A CompositeMatcher must only be constructed when all passed
+     * matchers are matching
+     */
+    private CompositeMatcher(List<Matcher> matchers) {
+        for (Matcher m : matchers){
+            checkArgument(m.isMatch(), "Non matching matcher found in [%s]", matchers);
+        }
+        this.matchers = matchers;
+    }
+
+    @Override
+    public Matcher next(String name) {
+        List<Matcher> nextSet = Lists.newArrayListWithCapacity(matchers.size());
+        for (Matcher current : matchers){
+            Matcher next = current.next(name);
+            if (next.isMatch()){
+                nextSet.add(next);
+            }
+        }
+        return compose(nextSet);
+    }
+
+    @Override
+    public boolean isMatch() {
+        return true;
+    }
+
+    @Override
+    public String getMatchedPath() {
+        //All matchers would have traversed same path. So use any one to
+        //determine the matching path
+        return matchers.get(0).getMatchedPath();
+    }
+
+    @Override
+    public int depth() {
+        return matchers.get(0).depth();
+    }
+
+    @Override
+    public boolean matchesAllChildren() {
+        for (Matcher m : matchers){
+            if (m.matchesAllChildren()){
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlor.java
new file mode 100644
index 0000000..44f1f94
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlor.java
@@ -0,0 +1,122 @@
+/*
+ * 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.bundlor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+
+public class DocumentBundlor {
+    /**
+     * Hidden property to store the pattern as part of NodeState
+     * TODO - Also store the NodeType
+     */
+    public static final String META_PROP_PATTERN = ":doc-pattern";
+
+    /**
+     * Hidden property name used as suffix for relative node path
+     * to indicate presence of that node. So for a relative node 'jcr:content'
+     * the parent node must have a property 'jcr:content/:self
+     */
+    public static final String META_PROP_NODE = ":doc-self";
+
+    public static final String HAS_CHILD_PROP_PREFIX = ":doc-has-child-";
+
+    /**
+     * Hidden property name having boolean value indicating that
+     * current node has children which are bundled
+     */
+    public static final String META_PROP_BUNDLED_CHILD = HAS_CHILD_PROP_PREFIX + "bundled";
+
+    /**
+     * Hidden property name having boolean value indicating that
+     * current node has children which are not bundled
+     */
+    public static final String META_PROP_NON_BUNDLED_CHILD = HAS_CHILD_PROP_PREFIX + "non-bundled";
+
+
+
+    public static final String PROP_PATTERN = "pattern";
+    private final List<Include> includes;
+
+    public static DocumentBundlor from(NodeState nodeState){
+        checkArgument(nodeState.hasProperty(PROP_PATTERN), "NodeState [%s] does not have required " +
+                "property [%s]", nodeState, PROP_PATTERN);
+       return DocumentBundlor.from(nodeState.getStrings(PROP_PATTERN));
+    }
+
+    public static DocumentBundlor from(Iterable<String> includeStrings){
+        List<Include> includes = Lists.newArrayList();
+        for (String i : includeStrings){
+            includes.add(new Include(i));
+        }
+        return new DocumentBundlor(includes);
+    }
+
+    public static DocumentBundlor from(PropertyState prop){
+        checkArgument(META_PROP_PATTERN.equals(prop.getName()));
+        return from(prop.getValue(Type.STRINGS));
+    }
+
+    private DocumentBundlor(List<Include> includes) {
+        checkArgument(!includes.isEmpty(), "Include list cannot be empty");
+        this.includes = ImmutableList.copyOf(includes);
+    }
+
+    public boolean isBundled(String relativePath) {
+        Matcher m = createMatcher();
+        for (String e : PathUtils.elements(relativePath)){
+            m = m.next(e);
+        }
+        return m.isMatch();
+    }
+
+    public PropertyState asPropertyState(){
+        List<String> includePatterns = new ArrayList<>(includes.size());
+        for (Include i : includes){
+            includePatterns.add(i.getPattern());
+        }
+        return createProperty(META_PROP_PATTERN, includePatterns, STRINGS);
+    }
+
+    public Matcher createMatcher(){
+        List<Matcher> matchers = Lists.newArrayListWithCapacity(includes.size());
+        for(Include include : includes){
+            matchers.add(include.createMatcher());
+        }
+        return CompositeMatcher.compose(matchers);
+    }
+
+    @Override
+    public String toString() {
+        return includes.toString();
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Include.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Include.java
new file mode 100644
index 0000000..ae254b1
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Include.java
@@ -0,0 +1,134 @@
+/*
+ * 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.bundlor;
+
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+/**
+ * Include represents a single path pattern which captures the path which
+ * needs to be included in bundling. Path patterns can be like below.
+ * <ul>
+ *     <li>* - Match any immediate child</li>
+ *     <li>*\/* - Match child with any name upto 2 levels of depth</li>
+ *     <li>jcr:content - Match immediate child with name jcr:content</li>
+ *     <li>jcr:content\/** - Match jcr:content and all its child</li>
+ * </ul>
+ *
+ * The last path element can specify a directive. Supported directive
+ * <ul>
+ *     <li>all - Include all nodes under given path</li>
+ * </ul>
+ */
+public class Include {
+    //TODO Restrict to * and have * == **
+    private static final String STAR = "*";
+    private static final String STAR_STAR = "**";
+
+    enum Directive {ALL, NONE}
+    private final String[] elements;
+    private final Directive directive;
+    private final String pattern;
+
+    public Include(String pattern){
+        List<String> pathElements = ImmutableList.copyOf(PathUtils.elements(pattern));
+        List<String> elementList = Lists.newArrayListWithCapacity(pathElements.size());
+        Directive directive = Directive.NONE;
+        for (int i = 0; i < pathElements.size(); i++) {
+            String e = pathElements.get(i);
+            int indexOfColon = e.indexOf(";");
+            if (indexOfColon > 0){
+                directive = Directive.valueOf(e.substring(indexOfColon + 1).toUpperCase());
+                e = e.substring(0, indexOfColon);
+            }
+
+            if (STAR_STAR.equals(e)){
+                e = STAR;
+                directive = Directive.ALL;
+            }
+
+            elementList.add(e);
+
+            if (directive != Directive.NONE && i < pathElements.size() - 1){
+                throw new IllegalArgumentException("Directive can only be specified for last path segment ["+pattern+"]");
+            }
+        }
+
+        this.elements = elementList.toArray(new String[0]);
+        this.directive = directive;
+        this.pattern = pattern;
+    }
+
+    public boolean match(String relativePath) {
+        Matcher m = createMatcher();
+        for (String e : PathUtils.elements(relativePath)){
+            m = m.next(e);
+        }
+        return m.isMatch();
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+
+    @Override
+    public String toString() {
+        return pattern;
+    }
+
+    public Matcher createMatcher() {
+        return new IncludeMatcher(this);
+    }
+
+    Directive getDirective() {
+        return directive;
+    }
+
+    /**
+     * Matches node name against pattern at given depth.
+     * Depth here would be 1 based with 0 for root depth
+     *
+     * @param nodeName nodeName to match
+     * @param depth depth in path
+     * @return true if nodeName matched against pattern at given depth
+     */
+    public boolean match(String nodeName, int depth) {
+        int elementIndex = depth - 1;
+        checkElementIndex(elementIndex, elements.length);
+        String e = elements[elementIndex];
+        return STAR.equals(e) || nodeName.equals(e);
+    }
+
+    public boolean matchAny(int depth){
+        int elementIndex = depth - 1;
+        checkElementIndex(elementIndex, elements.length);
+        return STAR.equals(elements[elementIndex]);
+    }
+
+    public int size() {
+        return elements.length;
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeAllMatcher.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeAllMatcher.java
new file mode 100644
index 0000000..1779eb1
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeAllMatcher.java
@@ -0,0 +1,67 @@
+/*
+ * 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.bundlor;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+
+/**
+ * Matcher which matches all child nodes
+ */
+class IncludeAllMatcher implements Matcher {
+    private final String matchingPath;
+    private final int depth;
+
+    IncludeAllMatcher(String matchingPath, int depth) {
+        checkArgument(depth > 0);
+        this.matchingPath = matchingPath;
+        this.depth = depth;
+    }
+
+    @Override
+    public Matcher next(String name) {
+        return new IncludeAllMatcher(concat(matchingPath, name), depth + 1);
+    }
+
+    @Override
+    public boolean isMatch() {
+        return true;
+    }
+
+    @Override
+    public String getMatchedPath() {
+        return matchingPath;
+    }
+
+    @Override
+    public int depth() {
+        return depth;
+    }
+
+    @Override
+    public boolean matchesAllChildren() {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "ALL - " + matchingPath;
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcher.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcher.java
new file mode 100644
index 0000000..483d7f0
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcher.java
@@ -0,0 +1,103 @@
+/*
+ * 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.bundlor;
+
+import static org.apache.jackrabbit.oak.commons.PathUtils.ROOT_NAME;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+
+class IncludeMatcher implements Matcher {
+    private final Include include;
+    /**
+     * Depth is 1 based i.e. first node element in path would have depth 1.
+     * Root has depth 0
+     */
+    private final int depth;
+    private final String matchedPath;
+
+    public IncludeMatcher(Include include) {
+        this(include, 0, ROOT_NAME);
+    }
+
+    private IncludeMatcher(Include include, int depth, String matchedPath) {
+        this.include = include;
+        this.depth = depth;
+        this.matchedPath = matchedPath;
+    }
+
+    @Override
+    public Matcher next(String name) {
+        if (hasMore()) {
+            if (include.match(name, nextElementIndex())) {
+                String nextPath = concat(matchedPath, name);
+                if (lastEntry() && include.getDirective() == Include.Directive.ALL) {
+                    return new IncludeAllMatcher(nextPath, nextElementIndex());
+                }
+                return new IncludeMatcher(include, nextElementIndex(), nextPath);
+            } else {
+                return Matcher.NON_MATCHING;
+            }
+        }
+        return Matcher.NON_MATCHING;
+    }
+
+    @Override
+    public boolean isMatch() {
+        return true;
+    }
+
+    @Override
+    public String getMatchedPath() {
+        return matchedPath;
+    }
+
+    @Override
+    public int depth() {
+        return depth;
+    }
+
+    @Override
+    public boolean matchesAllChildren() {
+        if (hasMore()){
+            return include.matchAny(nextElementIndex());
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "IncludeMatcher{" +
+                "include=" + include +
+                ", depth=" + depth +
+                ", matchedPath='" + matchedPath + '\'' +
+                '}';
+    }
+
+    private int nextElementIndex(){
+        return depth + 1;
+    }
+
+    private boolean hasMore() {
+        return depth < include.size();
+    }
+
+    private boolean lastEntry() {
+        return depth == include.size() - 1;
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Matcher.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Matcher.java
new file mode 100644
index 0000000..b8a8e68
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/Matcher.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.bundlor;
+
+public interface Matcher {
+    Matcher NON_MATCHING = new Matcher() {
+        @Override
+        public Matcher next(String name) {
+            return NON_MATCHING;
+        }
+
+        @Override
+        public boolean isMatch() {
+            return false;
+        }
+
+        @Override
+        public String getMatchedPath() {
+            throw new IllegalStateException("No matching path for non matching matcher");
+        }
+
+        @Override
+        public int depth() {
+            return 0;
+        }
+
+        @Override
+        public boolean matchesAllChildren() {
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "NON_MATCHING";
+        }
+    };
+
+    /**
+     * Returns a matcher for given child node name based on current state
+     *
+     * @param name child node name
+     * @return child matcher
+     */
+    Matcher next(String name);
+
+    /**
+     * Returns true if there was a match wrt current child node path
+     */
+    boolean isMatch();
+
+    /**
+     * Relative node path from the bundling root if
+     * there was a match
+     */
+    String getMatchedPath();
+
+    /**
+     * Matcher depth. For match done for 'x/y' depth is 2
+     */
+    int depth();
+
+    /**
+     * Returns true if matcher for all immediate child node
+     * would also be a matching matcher. This would be the
+     * case if IncludeMatcher with '*' or '**' as pattern for
+     * child nodes
+     */
+    boolean matchesAllChildren();
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/PropertyStateWrapper.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/PropertyStateWrapper.java
new file mode 100644
index 0000000..bcca0ef
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/bundlor/PropertyStateWrapper.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.bundlor;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.memory.AbstractPropertyState;
+
+//TODO Move this to org.apache.jackrabbit.oak.plugins.memory
+class PropertyStateWrapper extends AbstractPropertyState implements PropertyState {
+    private final PropertyState delegate;
+
+    public PropertyStateWrapper(PropertyState delegate) {
+        this.delegate = delegate;
+    }
+
+    @Nonnull
+    @Override
+    public String getName() {
+        return delegate.getName();
+    }
+
+    @Override
+    public boolean isArray() {
+        return delegate.isArray();
+    }
+
+    @Override
+    public Type<?> getType() {
+        return delegate.getType();
+    }
+
+    @Nonnull
+    @Override
+    public <T> T getValue(Type<T> type) {
+        return delegate.getValue(type);
+    }
+
+    @Nonnull
+    @Override
+    public <T> T getValue(Type<T> type, int index) {
+        return delegate.getValue(type, index);
+    }
+
+    @Override
+    public long size() {
+        return delegate.size();
+    }
+
+    @Override
+    public long size(int index) {
+        return delegate.size(index);
+    }
+
+    @Override
+    public int count() {
+        return delegate.count();
+    }
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/LocalDiffCacheTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/LocalDiffCacheTest.java
index 9d960dd..11fed28 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/LocalDiffCacheTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/LocalDiffCacheTest.java
@@ -20,9 +20,6 @@
 
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.Nonnull;
 
 import com.google.common.collect.Maps;
 
@@ -30,7 +27,6 @@
 import org.apache.jackrabbit.oak.cache.CacheStats;
 import org.apache.jackrabbit.oak.plugins.document.LocalDiffCache.Diff;
 import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
-import org.apache.jackrabbit.oak.plugins.observation.NodeObserver;
 import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
@@ -147,66 +143,4 @@ private static void resetStats(Iterable<CacheStats> stats) {
             cs.resetStats();
         }
     }
-
-    //------------------------------------------------------------< TestNodeObserver >---
-
-    private static class TestNodeObserver extends NodeObserver {
-        private final Map<String, Set<String>> added = newHashMap();
-        private final Map<String, Set<String>> deleted = newHashMap();
-        private final Map<String, Set<String>> changed = newHashMap();
-        private final Map<String, Map<String, String>> properties = newHashMap();
-
-        protected TestNodeObserver(String path, String... propertyNames) {
-            super(path, propertyNames);
-        }
-
-        @Override
-        protected void added(
-                @Nonnull String path,
-                @Nonnull Set<String> added,
-                @Nonnull Set<String> deleted,
-                @Nonnull Set<String> changed,
-                @Nonnull Map<String, String> properties,
-                @Nonnull CommitInfo commitInfo) {
-            this.added.put(path, newHashSet(added));
-            if (!properties.isEmpty()) {
-                this.properties.put(path, newHashMap(properties));
-            }
-        }
-
-        @Override
-        protected void deleted(
-                @Nonnull String path,
-                @Nonnull Set<String> added,
-                @Nonnull Set<String> deleted,
-                @Nonnull Set<String> changed,
-                @Nonnull Map<String, String> properties,
-                @Nonnull CommitInfo commitInfo) {
-            this.deleted.put(path, newHashSet(deleted));
-            if (!properties.isEmpty()) {
-                this.properties.put(path, newHashMap(properties));
-            }
-        }
-
-        @Override
-        protected void changed(
-                @Nonnull String path,
-                @Nonnull Set<String> added,
-                @Nonnull Set<String> deleted,
-                @Nonnull Set<String> changed,
-                @Nonnull Map<String, String> properties,
-                @Nonnull CommitInfo commitInfo) {
-            this.changed.put(path, newHashSet(changed));
-            if (!properties.isEmpty()) {
-                this.properties.put(path, newHashMap(properties));
-            }
-        }
-
-        public void reset(){
-            added.clear();
-            deleted.clear();
-            changed.clear();
-            properties.clear();
-        }
-    }
 }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/TestNodeObserver.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/TestNodeObserver.java
new file mode 100644
index 0000000..a209a07
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/TestNodeObserver.java
@@ -0,0 +1,91 @@
+/*
+ * 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 java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.apache.jackrabbit.oak.plugins.observation.NodeObserver;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+
+import static com.google.common.collect.Maps.newHashMap;
+import static com.google.common.collect.Sets.newHashSet;
+
+public class TestNodeObserver extends NodeObserver {
+    public final Map<String, Set<String>> added = newHashMap();
+    public final Map<String, Set<String>> deleted = newHashMap();
+    public final Map<String, Set<String>> changed = newHashMap();
+    public final Map<String, Map<String, String>> properties = newHashMap();
+
+    public TestNodeObserver(String path, String... propertyNames) {
+        super(path, propertyNames);
+    }
+
+    @Override
+    protected void added(
+            @Nonnull String path,
+            @Nonnull Set<String> added,
+            @Nonnull Set<String> deleted,
+            @Nonnull Set<String> changed,
+            @Nonnull Map<String, String> properties,
+            @Nonnull CommitInfo commitInfo) {
+        this.added.put(path, newHashSet(added));
+        if (!properties.isEmpty()) {
+            this.properties.put(path, newHashMap(properties));
+        }
+    }
+
+    @Override
+    protected void deleted(
+            @Nonnull String path,
+            @Nonnull Set<String> added,
+            @Nonnull Set<String> deleted,
+            @Nonnull Set<String> changed,
+            @Nonnull Map<String, String> properties,
+            @Nonnull CommitInfo commitInfo) {
+        this.deleted.put(path, newHashSet(deleted));
+        if (!properties.isEmpty()) {
+            this.properties.put(path, newHashMap(properties));
+        }
+    }
+
+    @Override
+    protected void changed(
+            @Nonnull String path,
+            @Nonnull Set<String> added,
+            @Nonnull Set<String> deleted,
+            @Nonnull Set<String> changed,
+            @Nonnull Map<String, String> properties,
+            @Nonnull CommitInfo commitInfo) {
+        this.changed.put(path, newHashSet(changed));
+        if (!properties.isEmpty()) {
+            this.properties.put(path, newHashMap(properties));
+        }
+    }
+
+    public void reset(){
+        added.clear();
+        deleted.clear();
+        changed.clear();
+        properties.clear();
+    }
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistryTest.java
new file mode 100644
index 0000000..cfc71cc
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundledTypesRegistryTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.bundlor;
+
+import java.util.Collections;
+
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.PROP_PATTERN;
+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.*;
+
+public class BundledTypesRegistryTest {
+
+    private NodeBuilder builder = EMPTY_NODE.builder();
+
+    @Test
+    public void basicSetup() throws Exception{
+        builder.child("nt:file").setProperty(createProperty(PROP_PATTERN, asList("jcr:content"), STRINGS));
+        BundledTypesRegistry registry = BundledTypesRegistry.from(builder.getNodeState());
+
+        assertNull(registry.getBundlor(EMPTY_NODE));
+        assertNotNull(registry.getBundlor(newNode("nt:file", false)));
+        assertNull(registry.getBundlor(newNode("nt:resource", false)));
+
+        DocumentBundlor bundlor = registry.getBundlor(newNode("nt:file", false));
+        assertTrue(bundlor.isBundled("jcr:content"));
+        assertFalse(bundlor.isBundled("foo"));
+    }
+
+    @Test
+    public void mixin() throws Exception{
+        builder.child("mix:foo").setProperty(createProperty(PROP_PATTERN, asList("jcr:content"), STRINGS));
+        BundledTypesRegistry registry = BundledTypesRegistry.from(builder.getNodeState());
+        assertNotNull(registry.getBundlor(newNode("mix:foo", true)));
+    }
+
+    @Test
+    public void mixinOverPrimaryType() throws Exception{
+        builder.child("mix:foo").setProperty(createProperty(PROP_PATTERN, asList("foo"), STRINGS));
+        builder.child("nt:file").setProperty(createProperty(PROP_PATTERN, asList("jcr:content"), STRINGS));
+        BundledTypesRegistry registry = BundledTypesRegistry.from(builder.getNodeState());
+
+        NodeBuilder b2 = EMPTY_NODE.builder();
+        setType("nt:file", false, b2);
+        setType("mix:foo", true, b2);
+
+        DocumentBundlor bundlor = registry.getBundlor(b2.getNodeState());
+
+        //Pattern based on mixin would be applicable
+        assertTrue(bundlor.isBundled("foo"));
+        assertFalse(bundlor.isBundled("jcr:content"));
+    }
+
+    private static NodeState newNode(String typeName, boolean mixin){
+        NodeBuilder builder = EMPTY_NODE.builder();
+        setType(typeName, mixin, builder);
+        return builder.getNodeState();
+    }
+
+    private static void setType(String typeName, boolean mixin, NodeBuilder builder) {
+        if (mixin) {
+            builder.setProperty(JCR_MIXINTYPES, Collections.singleton(typeName), Type.NAMES);
+        } else {
+            builder.setProperty(JCR_PRIMARYTYPE, typeName);
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandlerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandlerTest.java
new file mode 100644
index 0000000..abf141f
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigHandlerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.bundlor;
+
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.Type;
+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.junit.Test;
+
+import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class BundlingConfigHandlerTest {
+    private BundlingConfigHandler configHandler = new BundlingConfigHandler();
+    private MemoryNodeStore nodeStore = new MemoryNodeStore();
+
+    @Test
+    public void defaultSetup() throws Exception{
+        assertNotNull(configHandler.getRegistry());
+        assertNotNull(configHandler.newBundlingHandler());
+
+        //Close should work without init also
+        configHandler.close();
+    }
+
+    @Test
+    public void detectRegistryUpdate() throws Exception{
+        configHandler.initialize(nodeStore, sameThreadExecutor());
+        addBundlorConfigForAsset();
+
+        BundledTypesRegistry registry = configHandler.getRegistry();
+        assertEquals(1, registry.getBundlors().size());
+        DocumentBundlor assetBundlor = registry.getBundlors().get("app:Asset");
+        assertNotNull(assetBundlor);
+    }
+
+    private void addBundlorConfigForAsset() throws CommitFailedException {
+        NodeBuilder builder = nodeStore.getRoot().builder();
+        NodeBuilder bundlor = builder.child("jcr:system").child("documentstore").child("bundlor");
+        bundlor.child("app:Asset").setProperty(DocumentBundlor.PROP_PATTERN,
+                singletonList("metadata"), Type.STRINGS);
+        nodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializerTest.java
new file mode 100644
index 0000000..49be2eb
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingConfigInitializerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.bundlor;
+
+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.junit.Test;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent.INITIAL_CONTENT;
+import static org.junit.Assert.*;
+
+public class BundlingConfigInitializerTest {
+    private NodeState root = INITIAL_CONTENT;
+
+    @Test
+    public void bootstrapDefault() throws Exception{
+        NodeBuilder builder = root.builder();
+        BundlingConfigInitializer.INSTANCE.initialize(builder);
+
+        NodeState state = builder.getNodeState();
+        NodeState bundlor = NodeStateUtils.getNode(state, BundlingConfigHandler.CONFIG_PATH);
+        assertTrue(bundlor.exists());
+        assertTrue(bundlor.getChildNode("nt:file").exists());
+    }
+
+    @Test
+    public void noInitWhenJcrSystemNotPresent() throws Exception{
+        NodeBuilder builder = EMPTY_NODE.builder();
+        BundlingConfigInitializer.INSTANCE.initialize(builder);
+
+        NodeState state = builder.getNodeState();
+        NodeState bundlor = NodeStateUtils.getNode(state, BundlingConfigHandler.CONFIG_PATH);
+        assertFalse(bundlor.exists());
+    }
+
+    @Test
+    public void noInitIfPartialExists() throws Exception{
+        NodeBuilder builder = root.builder();
+        builder.child(JCR_SYSTEM).child(BundlingConfigHandler.DOCUMENT_NODE_STORE);
+
+        BundlingConfigInitializer.INSTANCE.initialize(builder);
+        NodeState state = builder.getNodeState();
+        NodeState bundlor = NodeStateUtils.getNode(state, BundlingConfigHandler.CONFIG_PATH);
+        ///jcr:system/documentstore was already present then
+        //no initialization should have happened
+        assertFalse(bundlor.exists());
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandlerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandlerTest.java
new file mode 100644
index 0000000..20d103c
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlingHandlerTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.bundlor;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class BundlingHandlerTest {
+
+
+    private NodeBuilder builder = EMPTY_NODE.builder();
+
+    @Test
+    public void defaultSetup() throws Exception {
+        BundlingHandler handler = new BundlingHandler(BundledTypesRegistry.from(EMPTY_NODE));
+        childBuilder(builder, "/x/y/z");
+        NodeState state = builder.getNodeState();
+
+        assertEquals("/", handler.getRootBundlePath());
+        assertTrue(handler.isBundlingRoot());
+        assertEquals("foo", handler.getPropertyPath("foo"));
+
+        BundlingHandler xh = childHandler(handler, state, "/x");
+        assertEquals("/x", xh.getRootBundlePath());
+        assertTrue(xh.isBundlingRoot());
+        assertEquals("foo", xh.getPropertyPath("foo"));
+
+        BundlingHandler xz = childHandler(handler, state, "/x/y/z");
+        assertEquals("/x/y/z", xz.getRootBundlePath());
+        assertTrue(xz.isBundlingRoot());
+        assertEquals("foo", xz.getPropertyPath("foo"));
+    }
+
+    @Test
+    public void ntFileBundled() throws Exception {
+        BundledTypesRegistry registry = BundledTypesRegistry.builder().forType("nt:file", "jcr:content").buildRegistry();
+
+        childBuilder(builder, "sunrise.jpg/jcr:content").setProperty("jcr:data", "foo");
+        childBuilder(builder, "sunrise.jpg/jcr:content/bar").setProperty("jcr:data", "foo");
+        type(childBuilder(builder, "sunrise.jpg"), "nt:file");
+        childBuilder(builder, "/sunrise.jpg/metadata").setProperty("name", "foo");
+
+        NodeState state = builder.getNodeState();
+        BundlingHandler handler = new BundlingHandler(registry);
+
+        BundlingHandler fileHandler = childHandler(handler, state, "/sunrise.jpg");
+        assertEquals("/sunrise.jpg", fileHandler.getRootBundlePath());
+        assertTrue(fileHandler.isBundlingRoot());
+        assertFalse(fileHandler.isBundledNode());
+        assertEquals("foo", fileHandler.getPropertyPath("foo"));
+
+        BundlingHandler jcrContentHandler = childHandler(handler, state, "/sunrise.jpg/jcr:content");
+        assertEquals("/sunrise.jpg", jcrContentHandler.getRootBundlePath());
+        assertFalse(jcrContentHandler.isBundlingRoot());
+        assertTrue(jcrContentHandler.isBundledNode());
+        assertEquals("jcr:content/foo", jcrContentHandler.getPropertyPath("foo"));
+
+        BundlingHandler metadataHandler = childHandler(handler, state, "/sunrise.jpg/metadata");
+        assertEquals("/sunrise.jpg/metadata", metadataHandler.getRootBundlePath());
+        assertTrue(metadataHandler.isBundlingRoot());
+        assertFalse(metadataHandler.isBundledNode());
+        assertEquals("foo", metadataHandler.getPropertyPath("foo"));
+
+        // /sunrise.jpg/jcr:content/bar should have bundle root reset
+        BundlingHandler barHandler = childHandler(handler, state, "/sunrise.jpg/jcr:content/bar");
+        assertEquals("/sunrise.jpg/jcr:content/bar", barHandler.getRootBundlePath());
+        assertTrue(barHandler.isBundlingRoot());
+        assertEquals("foo", barHandler.getPropertyPath("foo"));
+    }
+
+    @Test
+    public void childAdded_BundlingStart() throws Exception{
+        BundledTypesRegistry registry = BundledTypesRegistry.builder().forType("nt:file", "jcr:content").buildRegistry();
+
+        BundlingHandler handler = new BundlingHandler(registry);
+        childBuilder(builder, "sunrise.jpg/jcr:content").setProperty("jcr:data", "foo");
+        type(childBuilder(builder, "sunrise.jpg"), "nt:file");
+        NodeState state = builder.getNodeState();
+
+        BundlingHandler fileHandler = handler.childAdded("sunrise.jpg", state.getChildNode("sunrise.jpg"));
+        assertEquals("/sunrise.jpg", fileHandler.getRootBundlePath());
+        assertTrue(fileHandler.isBundlingRoot());
+        assertEquals("foo", fileHandler.getPropertyPath("foo"));
+        assertEquals(1, fileHandler.getMetaProps().size());
+    }
+    
+    @Test
+    public void childAdded_NoBundling() throws Exception{
+        BundlingHandler handler = new BundlingHandler(BundledTypesRegistry.from(EMPTY_NODE));
+        childBuilder(builder, "sunrise.jpg/jcr:content").setProperty("jcr:data", "foo");
+        type(childBuilder(builder, "sunrise.jpg"), "nt:file");
+        NodeState state = builder.getNodeState();
+
+        BundlingHandler fileHandler = handler.childAdded("sunrise.jpg", state.getChildNode("sunrise.jpg"));
+        assertEquals("/sunrise.jpg", fileHandler.getRootBundlePath());
+        assertTrue(fileHandler.isBundlingRoot());
+        assertEquals("foo", fileHandler.getPropertyPath("foo"));
+        assertEquals(0, fileHandler.getMetaProps().size());
+    }
+
+    private BundlingHandler childHandler(BundlingHandler parent, NodeState parentState, String childPath) {
+        BundlingHandler result = parent;
+        NodeState state = parentState;
+        for (String name : PathUtils.elements(checkNotNull(childPath))) {
+            state = state.getChildNode(name);
+            result = result.childAdded(name, state);
+        }
+        return result;
+    }
+
+    private NodeBuilder childBuilder(NodeBuilder parent, String childPath) {
+        NodeBuilder result = parent;
+        for (String name : PathUtils.elements(checkNotNull(childPath))) {
+            result = result.child(name);
+        }
+        return result;
+    }
+
+    private NodeBuilder type(NodeBuilder builder, String typeName) {
+        builder.setProperty(JCR_PRIMARYTYPE, typeName);
+        return builder;
+    }
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorConflictTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorConflictTest.java
new file mode 100644
index 0000000..fb0d1dd
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorConflictTest.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.bundlor;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+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.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.NodeStore;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.JcrConstants.NT_FILE;
+import static org.apache.jackrabbit.JcrConstants.NT_RESOURCE;
+
+public class BundlorConflictTest {
+
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+    private DocumentNodeStore store1;
+    private DocumentNodeStore store2;
+    private DocumentStore ds = new MemoryDocumentStore();
+
+    @Before
+    public void setUpBundlor() throws CommitFailedException {
+
+        store1 = builderProvider
+                .newBuilder()
+                .setDocumentStore(ds)
+                .memoryCacheSize(0)
+                .setClusterId(1)
+                .setAsyncDelay(0)
+                .getNodeStore();
+
+        store2 = builderProvider
+                .newBuilder()
+                .setDocumentStore(ds)
+                .memoryCacheSize(0)
+                .setClusterId(2)
+                .setAsyncDelay(0)
+                .getNodeStore();
+
+        NodeBuilder builder = store1.getRoot().builder();
+        NodeBuilder prevState = builder.child("oldState");
+
+        //Old state nt:file
+        createFile(prevState, "file");
+        //Old state app:Asset
+        createAsset(prevState, "assset");
+
+        merge(store1, builder);
+
+        NodeState registryState = BundledTypesRegistry.builder()
+                .forType("nt:file", "jcr:content")
+                .registry()
+                .forType("app:Asset")
+                .include("jcr:content")
+                .include("jcr:content/metadata")
+                .include("jcr:content/renditions")
+                .include("jcr:content/renditions/**")
+                .build();
+
+        builder = store1.getRoot().builder();
+        builder.child("jcr:system").child("documentstore").setChildNode("bundlor", registryState);
+        merge(store1, builder);
+
+        syncStores();
+    }
+
+    @Test
+    public void simpleConflict() throws Exception {
+        NodeBuilder root = store1.getRoot().builder();
+
+        getRendBuilder(createAsset(root, "foo")).getChildNode("rend-orig").setProperty("meta", "orig");
+        merge(store1, root);
+
+        syncStores();
+
+        NodeBuilder root1 = store1.getRoot().builder();
+        getRendBuilder(root1.getChildNode("foo")).child("rend1");//create a new rendition
+
+        NodeBuilder root2 = store2.getRoot().builder();
+        getRendBuilder(root2.getChildNode("foo")).remove();//remove rendition parent
+
+        merge(store1, root1);
+
+        thrown.expect(CommitFailedException.class);
+        merge(store2, root2);
+    }
+
+    private static void merge(NodeStore store,
+                              NodeBuilder root)
+            throws CommitFailedException {
+        store.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    private NodeBuilder createFile(NodeBuilder builder, String childName) {
+        NodeBuilder file = type(builder.child(childName), NT_FILE);
+        type(file.child("jcr:content"), NT_RESOURCE);
+
+        return file;
+    }
+
+    private NodeBuilder getRendBuilder(NodeBuilder assetBuilder) {
+        return assetBuilder.getChildNode("jcr:content").getChildNode("renditions");
+    }
+
+    private NodeBuilder createAsset(NodeBuilder builder, String childName) {
+        NodeBuilder asset = type(builder.child(childName), "app:Asset");
+        NodeBuilder assetJC = asset.child("jcr:content");
+        assetJC.child("metadata");
+        assetJC.child("renditions").child("rend-orig");
+        assetJC.child("comments");
+
+        return asset;
+    }
+
+    private NodeBuilder type(NodeBuilder builder, String typeName) {
+        builder.setProperty(JCR_PRIMARYTYPE, typeName);
+        return builder;
+    }
+
+    private void syncStores() {
+        store1.runBackgroundOperations();
+        store2.runBackgroundOperations();
+    }
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtilsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtilsTest.java
new file mode 100644
index 0000000..dc1995e
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/BundlorUtilsTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.bundlor;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.META_PROP_NODE;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.META_PROP_PATTERN;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class BundlorUtilsTest {
+    @Test
+    public void matchingProperties_Simple() throws Exception{
+        Map<String, PropertyState> result = BundlorUtils.getMatchingProperties(
+            create("a", "b", "c"), Matcher.NON_MATCHING
+        );
+
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void matchingProperties_BaseLevel() throws Exception{
+        Matcher m = new Include("jcr:content").createMatcher();
+        Map<String, PropertyState> result = BundlorUtils.getMatchingProperties(
+                create("a",
+                        concat("jcr:content", META_PROP_NODE),
+                        "jcr:content/jcr:data",
+                        "jcr:primaryType",
+                        META_PROP_PATTERN
+                ), m
+        );
+        assertThat(result.keySet(), hasItems("a", "jcr:primaryType", META_PROP_PATTERN));
+    }
+
+    @Test
+    public void matchingProperties_FirstLevel() throws Exception{
+        Matcher m = new Include("jcr:content").createMatcher().next("jcr:content");
+        Map<String, PropertyState> result = BundlorUtils.getMatchingProperties(
+                create("a",
+                        concat("jcr:content", META_PROP_NODE),
+                        "jcr:content/jcr:data",
+                        "jcr:content/metadata/format",
+                        "jcr:primaryType",
+                        META_PROP_PATTERN
+                ), m
+        );
+        assertThat(result.keySet(), hasItems("jcr:data"));
+        assertEquals("jcr:data", result.get("jcr:data").getName());
+    }
+
+    @Test
+    public void childNodeNames() throws Exception{
+        List<String> testData = asList("x",
+                concat("jcr:content", META_PROP_NODE),
+                "jcr:content/jcr:data",
+                concat("jcr:content/metadata", META_PROP_NODE),
+                "jcr:content/metadata/format",
+                concat("jcr:content/comments", META_PROP_NODE),
+                concat("jcr:content/renditions/original", META_PROP_NODE)
+        );
+
+        Matcher m = new Include("jcr:content/*").createMatcher();
+        List<String> names = BundlorUtils.getChildNodeNames(testData, m);
+        assertThat(names, hasItem("jcr:content"));
+
+        names = BundlorUtils.getChildNodeNames(testData, m.next("jcr:content"));
+        assertThat(names, hasItems("metadata", "comments"));
+
+        names = BundlorUtils.getChildNodeNames(testData, m.next("jcr:content").next("metadata"));
+        assertTrue(names.isEmpty());
+    }
+
+    private Map<String, PropertyState> create(String ... keyNames){
+        Map<String, PropertyState> map = new HashMap<>();
+        for (String key : keyNames){
+            map.put(key, PropertyStates.createProperty(key, Boolean.TRUE));
+        }
+        return map;
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcherTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcherTest.java
new file mode 100644
index 0000000..ac5d4d8
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/CompositeMatcherTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bundlor;
+
+import java.util.Collections;
+
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.*;
+
+public class CompositeMatcherTest {
+
+    @Test
+    public void empty() throws Exception{
+        Matcher m = CompositeMatcher.compose(Collections.<Matcher>emptyList());
+        assertFalse(m.isMatch());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void multiWithFailing() throws Exception{
+        CompositeMatcher.compose(asList(new Include("x").createMatcher(), Matcher.NON_MATCHING));
+    }
+
+    @Test
+    public void multi() throws Exception{
+        Matcher m = CompositeMatcher.compose(asList(
+                new Include("x/z").createMatcher(),
+                new Include("x/y").createMatcher())
+        );
+
+        Matcher m2 = m.next("x");
+        assertTrue(m2.isMatch());
+        assertEquals("x", m2.getMatchedPath());
+        assertEquals(1, m2.depth());
+
+        assertFalse(m.next("a").isMatch());
+
+        Matcher m3 = m2.next("y");
+        assertTrue(m3.isMatch());
+        assertEquals("x/y", m3.getMatchedPath());
+        assertEquals(2, m3.depth());
+
+        Matcher m4 = m3.next("a");
+        assertFalse(m4.isMatch());
+    }
+
+    @Test
+    public void matchChildren() throws Exception{
+        //Hypothetical case. First pattern is redundant
+        Matcher m = CompositeMatcher.compose(asList(
+                new Include("x/z").createMatcher(),
+                new Include("x/*").createMatcher())
+        );
+
+        assertFalse(m.matchesAllChildren());
+        assertTrue(m.next("x").matchesAllChildren());
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlingTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlingTest.java
new file mode 100644
index 0000000..6633530
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlingTest.java
@@ -0,0 +1,639 @@
+/*
+ * 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.bundlor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.document.Collection;
+import org.apache.jackrabbit.oak.plugins.document.Document;
+import org.apache.jackrabbit.oak.plugins.document.DocumentMKBuilderProvider;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeState;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.NodeDocument;
+import org.apache.jackrabbit.oak.plugins.document.TestNodeObserver;
+import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
+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 org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+import static java.lang.String.format;
+import static org.apache.commons.io.FileUtils.ONE_MB;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.META_PROP_BUNDLED_CHILD;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.spi.state.NodeStateUtils.getNode;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class DocumentBundlingTest {
+    @Rule
+    public DocumentMKBuilderProvider builderProvider = new DocumentMKBuilderProvider();
+    private DocumentNodeStore store;
+    private RecordingDocumentStore ds = new RecordingDocumentStore();
+
+    @Before
+    public void setUpBundlor() throws CommitFailedException {
+        store = builderProvider
+                .newBuilder()
+                .setDocumentStore(ds)
+                .memoryCacheSize(0)
+                .getNodeStore();
+        NodeState registryState = BundledTypesRegistry.builder()
+                .forType("nt:file", "jcr:content")
+                .registry()
+                .forType("app:Asset")
+                    .include("jcr:content")
+                    .include("jcr:content/metadata")
+                    .include("jcr:content/renditions")
+                    .include("jcr:content/renditions/**")
+                .build();
+
+        NodeBuilder builder = store.getRoot().builder();
+        builder.child("jcr:system").child("documentstore").setChildNode("bundlor", registryState);
+        merge(builder);
+    }
+
+    @Test
+    public void saveAndReadNtFile() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder fileNode = newNode("nt:file");
+        fileNode.child("jcr:content").setProperty("jcr:data", "foo");
+        builder.child("test").setChildNode("book.jpg", fileNode.getNodeState());
+
+        merge(builder);
+
+        NodeState root = store.getRoot();
+        NodeState fileNodeState = root.getChildNode("test");
+        assertTrue(fileNodeState.getChildNode("book.jpg").exists());
+        assertTrue(fileNodeState.getChildNode("book.jpg").getChildNode("jcr:content").exists());
+
+        assertNull(getNodeDocument("/test/book.jpg/jcr:content"));
+        assertNotNull(getNodeDocument("/test/book.jpg"));
+        assertTrue(hasNodeProperty("/test/book.jpg", concat("jcr:content", DocumentBundlor.META_PROP_NODE)));
+
+        AssertingDiff.assertEquals(fileNode.getNodeState(), fileNodeState.getChildNode("book.jpg"));
+
+        DocumentNodeState dns = asDocumentState(fileNodeState.getChildNode("book.jpg"));
+        AssertingDiff.assertEquals(fileNode.getNodeState(), dns.withRootRevision(dns.getRootRevision(), true));
+        AssertingDiff.assertEquals(fileNode.getNodeState(), dns.fromExternalChange());
+    }
+
+
+    @Test
+    public void bundledParent() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content", //Bundled
+                "jcr:content/comments" //Not bundled. Parent bundled
+        );
+        dump(appNB.getNodeState());
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+    }
+    
+    @Test
+    public void queryChildren() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content", 
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata",
+                "jcr:content/metadata/xmp", //not bundled
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+        NodeState appNode = getNode(store.getRoot(), "test/book.jpg");
+
+        ds.reset();
+
+        int childCount = Iterables.size(appNode.getChildNodeEntries());
+        assertEquals(1, childCount);
+        assertEquals(0, ds.queryPaths.size());
+
+        assertThat(childNames(appNode, "jcr:content"), hasItems("comments", "metadata", "renditions"));
+        assertEquals(3, getNode(appNode, "jcr:content").getChildNodeCount(100));
+
+        assertThat(childNames(appNode, "jcr:content/metadata"), hasItems("xmp"));
+        assertEquals(1, getNode(appNode, "jcr:content/metadata").getChildNodeCount(100));
+
+        ds.reset();
+        //For bundled case no query should be fired
+        assertThat(childNames(appNode, "jcr:content/renditions"), hasItems("original"));
+        assertEquals(1, getNode(appNode, "jcr:content/renditions").getChildNodeCount(100));
+        assertEquals(0, ds.queryPaths.size());
+
+        assertThat(childNames(appNode, "jcr:content/renditions/original"), hasItems("jcr:content"));
+        assertEquals(1, getNode(appNode, "jcr:content/renditions/original").getChildNodeCount(100));
+        assertEquals(0, ds.queryPaths.size());
+
+        AssertingDiff.assertEquals(appNB.getNodeState(), appNode);
+    }
+
+    @Test
+    public void hasChildren() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset"),
+                "jcr:content",
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata"
+        ).getNodeState());
+
+        ds.reset();
+
+        assertEquals(0, Iterables.size(getLatestNode("test/book.jpg/jcr:content/metadata").getChildNodeNames()));
+        assertEquals(0, ds.queryPaths.size());
+
+        //Case 1 - Bundled root but no bundled child
+        //Case 2 - Bundled root but non bundled child
+        //Case 3 - Bundled root but  bundled child
+        //Case 3 - Bundled node but  no bundled child
+        //Case 3 - Bundled leaf node but child can be present in non bundled form
+        //Case 3 - Bundled leaf node but all child bundled
+    }
+
+    @Test
+    public void hasChildren_BundledRoot_NoChild() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset")).getNodeState());
+
+        ds.reset();
+
+        assertEquals(0, Iterables.size(getLatestNode("test/book.jpg").getChildNodeNames()));
+        assertEquals(0, getLatestNode("test/book.jpg").getChildNodeCount(100));
+        assertEquals(0, ds.queryPaths.size());
+
+        NodeBuilder builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content");
+        merge(builder);
+
+        ds.reset();
+        assertEquals(1, Iterables.size(getLatestNode("test/book.jpg").getChildNodeNames()));
+        assertEquals(1, getLatestNode("test/book.jpg").getChildNodeCount(100));
+        assertEquals(0, ds.queryPaths.size());
+        assertTrue(hasNodeProperty("/test/book.jpg", META_PROP_BUNDLED_CHILD));
+        assertFalse(hasNodeProperty("/test/book.jpg", "_children"));
+    }
+
+    @Test
+    public void hasChildren_BundledRoot_BundledChild() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset"), "jcr:content").getNodeState());
+
+        ds.reset();
+
+        assertEquals(1, Iterables.size(getLatestNode("test/book.jpg").getChildNodeNames()));
+        assertEquals(0, ds.queryPaths.size());
+
+        NodeBuilder builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/fooContent");
+        merge(builder);
+
+        ds.reset();
+        assertEquals(2, Iterables.size(getLatestNode("test/book.jpg").getChildNodeNames()));
+        assertEquals(1, ds.queryPaths.size());
+
+        assertTrue(hasNodeProperty("/test/book.jpg", META_PROP_BUNDLED_CHILD));
+        assertTrue(hasNodeProperty("/test/book.jpg", "_children"));
+    }
+
+
+    @Test
+    public void hasChildren_BundledRoot_NonBundledChild() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset"), "fooContent").getNodeState());
+
+        ds.reset();
+
+        assertEquals(1, Iterables.size(getLatestNode("test/book.jpg").getChildNodeNames()));
+        assertEquals(1, ds.queryPaths.size());
+
+        assertFalse(hasNodeProperty("/test/book.jpg", META_PROP_BUNDLED_CHILD));
+        assertTrue(hasNodeProperty("/test/book.jpg", "_children"));
+    }
+
+    @Test
+    public void hasChildren_BundledNode_NoChild() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset"),
+                "jcr:content"
+        ).getNodeState());
+
+        ds.reset();
+
+        assertEquals(0, Iterables.size(getLatestNode("test/book.jpg/jcr:content").getChildNodeNames()));
+        assertEquals(0, ds.queryPaths.size());
+
+        NodeBuilder builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/metadata");
+        merge(builder);
+
+        ds.reset();
+        assertEquals(1, Iterables.size(getLatestNode("test/book.jpg/jcr:content").getChildNodeNames()));
+        assertEquals(0, ds.queryPaths.size());
+    }
+
+    @Test
+    public void hasChildren_BundledLeafNode_NoChild() throws Exception{
+        createTestNode("/test/book.jpg", createChild(newNode("app:Asset"),
+                "jcr:content/metadata"
+        ).getNodeState());
+
+        ds.reset();
+
+        assertEquals(0, Iterables.size(getLatestNode("test/book.jpg/jcr:content/metadata").getChildNodeNames()));
+        assertEquals(0, ds.queryPaths.size());
+
+        NodeBuilder builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/metadata/xmp");
+        merge(builder);
+
+        ds.reset();
+        assertEquals(1, Iterables.size(getLatestNode("test/book.jpg/jcr:content/metadata").getChildNodeNames()));
+        assertEquals(1, ds.queryPaths.size());
+    }
+
+    @Test
+    public void addBundledNodePostInitialCreation() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content",
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata",
+                "jcr:content/metadata/xmp", //not bundled
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+
+        builder = store.getRoot().builder();
+        NodeBuilder renditions = childBuilder(builder, "/test/book.jpg/jcr:content/renditions");
+        renditions.child("small").child("jcr:content");
+        NodeState appNode_v2 = childBuilder(builder, "/test/book.jpg").getNodeState();
+        merge(builder);
+
+        assertThat(childNames(getLatestNode("/test/book.jpg"), "jcr:content/renditions"),
+                hasItems("original", "small"));
+        assertTrue(AssertingDiff.assertEquals(getLatestNode("/test/book.jpg"), appNode_v2));
+    }
+
+    @Test
+    public void modifyBundledChild() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content",
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata",
+                "jcr:content/metadata/xmp", //not bundled
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+
+        //Modify bundled property
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content").setProperty("foo", "bar");
+        merge(builder);
+
+        NodeState state = childBuilder(builder, "/test/book.jpg").getNodeState();
+        assertEquals("bar", getLatestNode("/test/book.jpg/jcr:content").getString("foo"));
+        assertTrue(AssertingDiff.assertEquals(state, getLatestNode("/test/book.jpg")));
+
+        //Modify deep bundled property
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/renditions").setProperty("foo", "bar");
+        merge(builder);
+
+        state = childBuilder(builder, "/test/book.jpg").getNodeState();
+        assertEquals("bar", getLatestNode("/test/book.jpg/jcr:content/renditions").getString("foo"));
+        assertTrue(AssertingDiff.assertEquals(state, getLatestNode("/test/book.jpg")));
+
+        //Modify deep unbundled property - jcr:content/comments/@foo
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/comments").setProperty("foo", "bar");
+        merge(builder);
+
+        state = childBuilder(builder, "/test/book.jpg").getNodeState();
+        assertEquals("bar", getLatestNode("/test/book.jpg/jcr:content/comments").getString("foo"));
+        assertTrue(AssertingDiff.assertEquals(state, getLatestNode("/test/book.jpg")));
+    }
+
+    @Test
+    public void deleteBundledNode() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content",
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata",
+                "jcr:content/metadata/xmp", //not bundled
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+
+        childBuilder(appNB, "jcr:content/metadata").setProperty("foo", "bar");
+        childBuilder(appNB, "jcr:content/comments").setProperty("foo", "bar");
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+
+        //Delete a bundled node jcr:content/metadata
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/metadata").remove();
+        NodeState appNode_v2 = childBuilder(builder, "/test/book.jpg").getNodeState();
+        merge(builder);
+
+        assertTrue(AssertingDiff.assertEquals(appNode_v2, getLatestNode("/test/book.jpg")));
+
+
+        //Delete unbundled child jcr:content/comments
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/comments").remove();
+        NodeState appNode_v3 = childBuilder(builder, "/test/book.jpg").getNodeState();
+        merge(builder);
+
+        assertTrue(AssertingDiff.assertEquals(appNode_v3, getLatestNode("/test/book.jpg")));
+
+    }
+
+    @Test
+    public void binaryFlagSet() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content",
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+
+
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+        merge(builder);
+
+        assertFalse(getNodeDocument("/test/book.jpg").hasBinary());
+
+        builder = store.getRoot().builder();
+        childBuilder(builder, "test/book.jpg/jcr:content/renditions/original/jcr:content").setProperty("foo", "bar".getBytes());
+        merge(builder);
+        assertTrue(getNodeDocument("/test/book.jpg").hasBinary());
+    }
+
+    @Test
+    public void jsonSerialization() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder appNB = newNode("app:Asset");
+        createChild(appNB,
+                "jcr:content",
+                "jcr:content/comments", //not bundled
+                "jcr:content/metadata",
+                "jcr:content/metadata/xmp", //not bundled
+                "jcr:content/renditions", //includes all
+                "jcr:content/renditions/original",
+                "jcr:content/renditions/original/jcr:content"
+        );
+        builder.child("test").setChildNode("book.jpg", appNB.getNodeState());
+
+        merge(builder);
+        DocumentNodeState appNode = (DocumentNodeState) getNode(store.getRoot(), "test/book.jpg");
+        String json = appNode.asString();
+        NodeState appNode2 = DocumentNodeState.fromString(store, json);
+        AssertingDiff.assertEquals(appNode, appNode2);
+    }
+
+    @Test
+    public void bundledDocsShouldNotBePartOfBackgroundUpdate() throws Exception{
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder fileNode = newNode("nt:file");
+        fileNode.child("jcr:content").setProperty("jcr:data", "foo");
+        builder.child("test").setChildNode("book.jpg", fileNode.getNodeState());
+
+        merge(builder);
+        builder = store.getRoot().builder();
+        childBuilder(builder, "/test/book.jpg/jcr:content/vlt:definition").setProperty("foo", "bar");
+        merge(builder);
+
+        //2 for /test, /test/book.jpg and /test/book.jpg/jcr:content should be there
+        //TODO If UnsavedModification is made public we can assert on path itself
+        assertEquals(2, store.getPendingWriteCount());
+    }
+
+    @Test
+    public void bundledNodeAndNodeCache() throws Exception{
+        store.dispose();
+        store = builderProvider
+                .newBuilder()
+                .setDocumentStore(ds)
+                .memoryCacheSize(ONE_MB * 10)
+                .getNodeStore();
+
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder fileNode = newNode("nt:file");
+        fileNode.child("jcr:content").setProperty("jcr:data", "foo");
+        builder.child("test").setChildNode("book.jpg", fileNode.getNodeState());
+
+        //Make some modifications under the bundled node
+        //This would cause an entry for bundled node in Commit modified set
+        childBuilder(builder, "/test/book.jpg/jcr:content/vlt:definition").setProperty("foo", "bar");
+
+        TestNodeObserver o = new TestNodeObserver("test");
+        store.addObserver(o);
+        merge(builder);
+        assertEquals(4, o.added.size());
+
+    }
+
+    private void createTestNode(String path, NodeState state) throws CommitFailedException {
+        String parentPath = PathUtils.getParentPath(path);
+        NodeBuilder builder = store.getRoot().builder();
+        NodeBuilder parent = childBuilder(builder, parentPath);
+        parent.setChildNode(PathUtils.getName(path), state);
+        merge(builder);
+    }
+
+    private NodeState getLatestNode(String path){
+        return getNode(store.getRoot(), path);
+    }
+
+    private boolean hasNodeProperty(String path, String propName) {
+        NodeDocument doc = getNodeDocument(path);
+        return doc.keySet().contains(propName);
+    }
+
+    private NodeDocument getNodeDocument(String path) {
+        return ds.find(Collection.NODES, Utils.getIdFromPath(path));
+    }
+
+    private void merge(NodeBuilder builder) throws CommitFailedException {
+        store.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    private static DocumentNodeState asDocumentState(NodeState state){
+        if (state instanceof DocumentNodeState){
+            return (DocumentNodeState) state;
+        }
+        fail("Not of type AbstractDoucmentNodeState");
+        return null;
+    }
+
+    private static void dump(NodeState state){
+        System.out.println(NodeStateUtils.toString(state));
+    }
+
+    private static List<String> childNames(NodeState state, String path){
+        return copyOf(getNode(state, path).getChildNodeNames());
+    }
+
+    private static NodeBuilder newNode(String typeName){
+        NodeBuilder builder = EMPTY_NODE.builder();
+        builder.setProperty(JCR_PRIMARYTYPE, typeName);
+        return builder;
+    }
+
+    private static NodeBuilder createChild(NodeBuilder root, String ... paths){
+        for (String path : paths){
+            childBuilder(root, path);
+        }
+        return root;
+    }
+
+    private static NodeBuilder childBuilder(NodeBuilder root, String path){
+        NodeBuilder nb = root;
+        for (String nodeName : PathUtils.elements(path)){
+            nb = nb.child(nodeName);
+        }
+        return nb;
+    }
+
+    private static class RecordingDocumentStore extends MemoryDocumentStore {
+        final List<String> queryPaths = new ArrayList<>();
+        final List<String> findPaths = new ArrayList<>();
+
+        @Override
+        public <T extends Document> T find(Collection<T> collection, String key, int maxCacheAge) {
+            if (collection == Collection.NODES){
+                findPaths.add(Utils.getPathFromId(key));
+            }
+            return super.find(collection, key);
+        }
+
+        @Nonnull
+        @Override
+        public <T extends Document> List<T> query(Collection<T> collection, String fromKey, String toKey,
+                                                  String indexedProperty, long startValue, int limit) {
+            if (collection == Collection.NODES){
+                queryPaths.add(Utils.getPathFromId(Utils.getParentIdFromLowerLimit(fromKey)));
+            }
+            return super.query(collection, fromKey, toKey, indexedProperty, startValue, limit);
+        }
+
+        public void reset(){
+            queryPaths.clear();
+            findPaths.clear();
+        }
+    }
+
+    private static class AssertingDiff implements NodeStateDiff {
+        private final Set<String> ignoredProps = ImmutableSet.of(
+                DocumentBundlor.META_PROP_PATTERN,
+                META_PROP_BUNDLED_CHILD,
+                DocumentBundlor.META_PROP_NON_BUNDLED_CHILD
+        );
+
+        public static boolean assertEquals(NodeState before, NodeState after) {
+            //Do not rely on default compareAgainstBaseState as that works at lastRev level
+            //and we need proper equals
+            return before.exists() == after.exists()
+                    && AbstractNodeState.compareAgainstBaseState(after, before, new AssertingDiff());
+        }
+
+        @Override
+        public boolean propertyAdded(PropertyState after) {
+            if (ignore(after)) return true;
+            throw new AssertionError("Added property: " + after);
+        }
+
+        @Override
+        public boolean propertyChanged(PropertyState before, PropertyState after) {
+            if (ignore(after)) return true;
+            throw new AssertionError("Property changed: Before " + before + " , after " + after);
+        }
+
+        @Override
+        public boolean propertyDeleted(PropertyState before) {
+            if (ignore(before)) return true;
+            throw new AssertionError("Deleted property: " + before);
+        }
+
+        @Override
+        public boolean childNodeAdded(String name, NodeState after) {
+            throw new AssertionError(format("Added child: %s -  %s", name, after));
+        }
+
+        @Override
+        public boolean childNodeChanged(String name, NodeState before, NodeState after) {
+            return AbstractNodeState.compareAgainstBaseState(after, before, new AssertingDiff());
+        }
+
+        @Override
+        public boolean childNodeDeleted(String name, NodeState before) {
+            throw new AssertionError(format("Deleted child : %s -  %s", name, before));
+        }
+
+        private boolean ignore(PropertyState state){
+            return ignoredProps.contains(state.getName());
+        }
+    }
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlorTest.java
new file mode 100644
index 0000000..2da98bd
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/DocumentBundlorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.bundlor;
+
+import java.util.Collections;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.document.bundlor.DocumentBundlor.PROP_PATTERN;
+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.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class DocumentBundlorTest {
+
+    private NodeBuilder builder = EMPTY_NODE.builder();
+
+    @Test
+    public void basicSetup() throws Exception{
+        builder.setProperty(createProperty(PROP_PATTERN, asList("x", "x/y"), STRINGS));
+        DocumentBundlor bundlor = DocumentBundlor.from(builder.getNodeState());
+
+        assertTrue(bundlor.isBundled("x"));
+        assertTrue(bundlor.isBundled("x/y"));
+        assertFalse(bundlor.isBundled("x/y/z"));
+        assertFalse(bundlor.isBundled("z"));
+    }
+    
+    @Test(expected = IllegalArgumentException.class)
+    public void invalid() throws Exception{
+        DocumentBundlor.from(builder.getNodeState());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalid2() throws Exception{
+        DocumentBundlor.from(Collections.<String>emptyList());
+    }
+
+    @Test
+    public void asPropertyState() throws Exception{
+        builder.setProperty(createProperty(PROP_PATTERN, asList("x", "x/y", "z"), STRINGS));
+        DocumentBundlor bundlor = DocumentBundlor.from(builder.getNodeState());
+        PropertyState ps = bundlor.asPropertyState();
+
+        assertNotNull(ps);
+        DocumentBundlor bundlor2 = DocumentBundlor.from(ps);
+        assertTrue(bundlor2.isBundled("x"));
+        assertTrue(bundlor2.isBundled("x/y"));
+        assertFalse(bundlor2.isBundled("m"));
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcherTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcherTest.java
new file mode 100644
index 0000000..feab482
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeMatcherTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.bundlor;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class IncludeMatcherTest {
+
+    @Test
+    public void singleLevel() throws Exception{
+        Matcher m = new Include("x").createMatcher();
+        assertTrue(m.isMatch());
+        assertTrue(m.next("x").isMatch());
+        assertEquals("x", m.next("x").getMatchedPath());
+
+        //Next level does not match
+        assertFalse(m.next("x").next("x").isMatch());
+
+        //Same level different path element name does not match
+        assertFalse(m.next("y").isMatch());
+    }
+
+    @Test
+    public void includeAll() throws Exception{
+        Matcher m = new Include("x/**").createMatcher();
+
+        assertTrue(m.isMatch());
+        assertTrue(m.next("x").isMatch());
+        assertTrue(m.next("x").next("x").isMatch());
+        assertTrue(m.next("x").next("y").isMatch());
+        assertTrue(m.next("x").next("y").next("z").isMatch());
+
+        assertEquals("x/y/z", m.next("x").next("y").next("z").getMatchedPath());
+    }
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeTest.java
new file mode 100644
index 0000000..e6906aa
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/IncludeTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.bundlor;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class IncludeTest {
+
+    @Test
+    public void simpleWildcard() throws Exception{
+        Include i = new Include("*");
+        assertTrue(i.match("x"));
+        assertTrue(i.match("/x"));
+        assertFalse(i.match("/x/y"));
+    }
+
+    @Test
+    public void exactName() throws Exception{
+        assertTrue(new Include("x").match("x"));
+        assertFalse(new Include("x").match("y"));
+
+        assertTrue(new Include("x/y").match("x"));
+        assertFalse(new Include("x/y").match("y"));
+        assertTrue(new Include("x/y").match("x/y"));
+    }
+
+    @Test
+    public void directive() throws Exception{
+        Include i0 = new Include("x/*");
+        assertEquals(Include.Directive.NONE, i0.getDirective());
+
+        Include i = new Include("x/**");
+        assertEquals(Include.Directive.ALL, i.getDirective());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidDirective() throws Exception{
+        new Include("x/y;all/z");
+    }
+
+    @Test
+    public void directiveAll() throws Exception{
+        Include i = new Include("x/**");
+        assertTrue(i.match("x/y"));
+        assertTrue(i.match("x/y/z"));
+        assertTrue(i.match("x/y/z/x"));
+
+        Include i2 = new Include("x/y/**");
+        assertTrue(i2.match("x/y"));
+        assertTrue(i2.match("x/y/z"));
+        assertTrue(i2.match("x/y/z/x"));
+    }
+
+    @Test
+    public void depth() throws Exception{
+        Include i0 = new Include("x/*");
+        assertEquals(0, i0.createMatcher().depth());
+        assertEquals(1, i0.createMatcher().next("x").depth());
+        assertEquals(2, i0.createMatcher().next("x").next("y").depth());
+
+        // x/y/z would not match so depth should be 0
+        assertEquals(0, i0.createMatcher().next("x").next("y").next("z").depth());
+
+        Include i2 = new Include("x/y/**");
+        assertEquals(0, i2.createMatcher().depth());
+        assertEquals(1, i2.createMatcher().next("x").depth());
+        assertEquals(2, i2.createMatcher().next("x").next("y").depth());
+        assertEquals(3, i2.createMatcher().next("x").next("y").next("z").depth());
+    }
+
+    @Test
+    public void matchChildren() throws Exception{
+        Include i0 = new Include("x/*");
+        Matcher m = i0.createMatcher();
+        assertFalse(m.matchesAllChildren());
+        assertTrue(m.next("x").matchesAllChildren());
+
+        Include i1 = new Include("x/**");
+        m = i1.createMatcher();
+        assertFalse(m.matchesAllChildren());
+        assertTrue(m.next("x").matchesAllChildren());
+
+    }
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/MatcherTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/MatcherTest.java
new file mode 100644
index 0000000..f88350c
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/bundlor/MatcherTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.bundlor;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class MatcherTest {
+
+    @Test
+    public void failingMatcher() throws Exception{
+        assertFalse(Matcher.NON_MATCHING.isMatch());
+        assertFalse(Matcher.NON_MATCHING.next("x").isMatch());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void failingMatcherInvalidPath() throws Exception{
+        Matcher.NON_MATCHING.getMatchedPath();
+    }
+
+    @Test
+    public void includeAll() throws Exception{
+        Matcher m = new IncludeAllMatcher("x", 1);
+        assertTrue(m.isMatch());
+        assertTrue(m.matchesAllChildren());
+        assertEquals("x", m.getMatchedPath());
+        assertEquals(1, m.depth());
+
+        assertTrue(m.next("y").isMatch());
+        assertTrue(m.next("y").matchesAllChildren());
+        assertEquals("x/y", m.next("y").getMatchedPath());
+        assertEquals(2, m.next("y").depth());
+    }
+
+
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/CacheInvalidationIT.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/CacheInvalidationIT.java
index e398bc0..7c75cad 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/CacheInvalidationIT.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/CacheInvalidationIT.java
@@ -199,7 +199,8 @@ private DocumentNodeStore createNS(int clusterId) throws Exception {
                           .setMongoDB(mc.getDB())
                           .setClusterId(clusterId)
                           //Set delay to 0 so that effect of changes are immediately reflected
-                          .setAsyncDelay(0) 
+                          .setAsyncDelay(0)
+                          .setBundlingDisabled(true)
                           .setLeaseCheck(false)
                           .getNodeStore();
     }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/JournalIT.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/JournalIT.java
index 000ba1b..31eb2dd 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/JournalIT.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/mongo/JournalIT.java
@@ -214,7 +214,7 @@ protected DocumentMK createMK(int clusterId, int asyncDelay) {
         DB db = connectionFactory.getConnection().getDB();
         builder = newDocumentMKBuilder();
         return register(builder.setMongoDB(db)
-                .setClusterId(clusterId).setAsyncDelay(asyncDelay).open());
+                .setClusterId(clusterId).setAsyncDelay(asyncDelay).setBundlingDisabled(true).open());
     }
 
     private static long getCacheElementCount(DocumentStore ds) {
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakDocumentMemRepositoryStub.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakDocumentMemRepositoryStub.java
index 66fbbfb..9b06b1e 100644
--- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakDocumentMemRepositoryStub.java
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakDocumentMemRepositoryStub.java
@@ -24,6 +24,7 @@
 
 import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
 import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigInitializer;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
 import org.apache.jackrabbit.test.RepositoryStubException;
 
@@ -49,7 +50,7 @@ public OakDocumentMemRepositoryStub(Properties settings)
             store = new DocumentMK.Builder().getNodeStore();
             QueryEngineSettings qs = new QueryEngineSettings();
             qs.setFullTextComparisonWithoutIndex(true);
-            this.repository = new Jcr(store).with(qs).createRepository();
+            this.repository = new Jcr(store).with(qs).with(BundlingConfigInitializer.INSTANCE).createRepository();
 
             session = getRepository().login(superuser);
             TestContentLoader loader = new TestContentLoader();
diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakMongoNSRepositoryStub.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakMongoNSRepositoryStub.java
index 9c50c9a..edf900e 100644
--- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakMongoNSRepositoryStub.java
+++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OakMongoNSRepositoryStub.java
@@ -25,6 +25,7 @@
 import org.apache.jackrabbit.oak.plugins.document.DocumentMK;
 import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
 import org.apache.jackrabbit.oak.plugins.document.MongoUtils;
+import org.apache.jackrabbit.oak.plugins.document.bundlor.BundlingConfigInitializer;
 import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
 
@@ -63,7 +64,7 @@ public OakMongoNSRepositoryStub(Properties settings) throws RepositoryException
                     getNodeStore();
             QueryEngineSettings qs = new QueryEngineSettings();
             qs.setFullTextComparisonWithoutIndex(true);
-            this.repository = new Jcr(store).with(qs).createRepository();
+            this.repository = new Jcr(store).with(qs).with(BundlingConfigInitializer.INSTANCE).createRepository();
 
             session = getRepository().login(superuser);
             TestContentLoader loader = new TestContentLoader();
diff --git a/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/DocumentNodeStoreConfigTest.groovy b/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/DocumentNodeStoreConfigTest.groovy
index 31b90f3..e22fd53 100644
--- a/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/DocumentNodeStoreConfigTest.groovy
+++ b/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/DocumentNodeStoreConfigTest.groovy
@@ -265,6 +265,45 @@ class DocumentNodeStoreConfigTest extends AbstractRepositoryFactoryTest {
         testDocumentStoreStats(ns)
     }
 
+    @Test
+    public void testBundlingEnabledByDefault() throws Exception {
+        registry = repositoryFactory.initializeServiceRegistry(config)
+
+        //1. Register the DataSource as a service
+        DataSource ds = createDS("jdbc:h2:mem:testRDB;DB_CLOSE_DELAY=-1")
+        registry.registerService(DataSource.class.name, ds, ['datasource.name': 'oak'] as Hashtable)
+
+        //2. Create config for DocumentNodeStore with RDB enabled
+        createConfig([
+                'org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreService': [
+                        documentStoreType: 'RDB'
+                ]
+        ])
+
+        DocumentNodeStore ns = getServiceWithWait(NodeStore.class)
+        assert ns.bundlingConfigHandler.enabled
+    }
+
+    @Test
+    public void testBundlingDisabled() throws Exception {
+        registry = repositoryFactory.initializeServiceRegistry(config)
+
+        //1. Register the DataSource as a service
+        DataSource ds = createDS("jdbc:h2:mem:testRDB;DB_CLOSE_DELAY=-1")
+        registry.registerService(DataSource.class.name, ds, ['datasource.name': 'oak'] as Hashtable)
+
+        //2. Create config for DocumentNodeStore with RDB enabled
+        createConfig([
+                'org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreService': [
+                        documentStoreType: 'RDB',
+                        bundlingDisabled : true
+                ]
+        ])
+
+        DocumentNodeStore ns = getServiceWithWait(NodeStore.class)
+        assert !ns.bundlingConfigHandler.enabled
+    }
+
     private void testDocumentStoreStats(DocumentNodeStore store) {
         DocumentStoreStatsMBean stats = getService(DocumentStoreStatsMBean.class)
 
