diff --git a/oak-core/pom.xml b/oak-core/pom.xml
index b3562cb..64802c4 100644
--- a/oak-core/pom.xml
+++ b/oak-core/pom.xml
@@ -75,6 +75,7 @@
               org.apache.jackrabbit.oak.plugins.observation,
               org.apache.jackrabbit.oak.plugins.observation.filter,
               org.apache.jackrabbit.oak.plugins.tree,
+              org.apache.jackrabbit.oak.plugins.document.spi,
               org.apache.jackrabbit.oak.plugins.value,
               org.apache.jackrabbit.oak.plugins.version,
               org.apache.jackrabbit.oak.spi.commit,
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 6ee15c4..338b68a 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
@@ -583,6 +583,8 @@ private static void append(DocumentNodeState node,
         private Map<CacheType, PersistentCacheStats> persistentCacheStats =
                 new EnumMap<CacheType, PersistentCacheStats>(CacheType.class);
         private boolean bundlingDisabled;
+        private JournalPropertyServiceTracker journalServicePropertyTracker =
+                new JournalPropertyServiceTracker();
 
         public Builder() {
         }
@@ -1085,6 +1087,15 @@ public boolean isPrefetchExternalChanges() {
             return prefetchExternalChanges;
         }
 
+        public Builder setJournalServicePropertyTracker(JournalPropertyServiceTracker tracker) {
+            journalServicePropertyTracker = tracker;
+            return this;
+        }
+
+        public JournalPropertyServiceTracker getJournalServicePropertyTracker() {
+            return journalServicePropertyTracker;
+        }
+
         VersionGCSupport createVersionGCSupport() {
             DocumentStore store = getDocumentStore();
             if (store instanceof MongoDocumentStore) {
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 a032a5e..6c57a3e 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
@@ -449,6 +449,8 @@ public Long apply(@Nullable String input) {
 
     private final BundledDocumentDiffer bundledDocDiffer = new BundledDocumentDiffer(this);
 
+    private final JournalPropertyServiceTracker journalPropertyServiceTracker;
+
     public DocumentNodeStore(DocumentMK.Builder builder) {
         this.blobStore = builder.getBlobStore();
         this.statisticsProvider = builder.getStatisticsProvider();
@@ -491,6 +493,7 @@ public DocumentNodeStore(DocumentMK.Builder builder) {
             clusterNodeInfo.setLeaseCheckDisabled(true);
         }
 
+        this.journalPropertyServiceTracker = builder.getJournalServicePropertyTracker();
         this.store = s;
         this.changes = newJournalEntry();
         this.clusterId = cid;
@@ -793,6 +796,7 @@ public void headOfQueue(@Nonnull Revision revision) {
                         c.applyToCache(before, false);
                         // track modified paths
                         changes.modified(c.getModifiedPaths());
+                        changes.readFrom(info);
                         changes.addChangeSet(getChangeSet(info));
                         // update head revision
                         Revision r = c.getRevision();
@@ -1968,6 +1972,7 @@ BackgroundReadStats backgroundRead() {
         Map<Integer, Revision> lastRevMap = doc.getLastRev();
         try {
             ChangeSetBuilder changeSetBuilder = newChangeSetBuilder();
+            JournalPropertyHandler journalPropertyHandler = journalPropertyServiceTracker.newHandler();
             RevisionVector headRevision = getHeadRevision();
             Set<Revision> externalChanges = Sets.newHashSet();
             for (Map.Entry<Integer, Revision> e : lastRevMap.entrySet()) {
@@ -1991,7 +1996,7 @@ BackgroundReadStats backgroundRead() {
                     if (externalSort != null) {
                         // add changes for this particular clusterId to the externalSort
                         try {
-                            fillExternalChanges(externalSort, PathUtils.ROOT_PATH, last, r, store, changeSetBuilder);
+                            fillExternalChanges(externalSort, PathUtils.ROOT_PATH, last, r, store, changeSetBuilder, journalPropertyHandler);
                         } catch (IOException e1) {
                             LOG.error("backgroundRead: Exception while reading external changes from journal: " + e1, e1);
                             IOUtils.closeQuietly(externalSort);
@@ -2050,7 +2055,7 @@ BackgroundReadStats backgroundRead() {
 
                     ChangeSet changeSet = changeSetBuilder.build();
                     LOG.debug("Dispatching external change with ChangeSet {}", changeSet);
-                    dispatcher.contentChanged(getRoot().fromExternalChange(), newCommitInfo(changeSet));
+                    dispatcher.contentChanged(getRoot().fromExternalChange(), newCommitInfo(changeSet, journalPropertyHandler));
                 } finally {
                     backgroundOperationLock.writeLock().unlock();
                 }
@@ -2063,9 +2068,10 @@ BackgroundReadStats backgroundRead() {
         return stats;
     }
 
-    private static CommitInfo newCommitInfo(@Nonnull  ChangeSet changeSet) {
+    private static CommitInfo newCommitInfo(@Nonnull ChangeSet changeSet, JournalPropertyHandler journalPropertyHandler) {
         CommitContext commitContext = new SimpleCommitContext();
         commitContext.set(COMMIT_CONTEXT_OBSERVATION_CHANGESET, changeSet);
+        journalPropertyHandler.addTo(commitContext);
         Map<String, Object> info = ImmutableMap.<String, Object>of(CommitContext.NAME, commitContext);
         return new CommitInfo(CommitInfo.OAK_UNKNOWN, CommitInfo.OAK_UNKNOWN, info, true);
     }
@@ -2210,7 +2216,7 @@ private long getBinarySize(@Nullable String json) {
     }
 
     private JournalEntry newJournalEntry() {
-        return new JournalEntry(store, true, newChangeSetBuilder());
+        return new JournalEntry(store, true, newChangeSetBuilder(), journalPropertyServiceTracker.newHandler());
     }
 
     /**
@@ -2941,4 +2947,8 @@ public DocumentNodeStateCache getNodeStateCache() {
     public void setNodeStateCache(DocumentNodeStateCache nodeStateCache) {
         this.nodeStateCache = nodeStateCache;
     }
+
+    public JournalPropertyServiceTracker getJournalPropertyServiceTracker() {
+        return journalPropertyServiceTracker;
+    }
 }
\ No newline at end of file
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 7c0a138..559e283 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
@@ -292,6 +292,7 @@ static DocumentStoreType fromString(String type) {
 
     private DocumentNodeStore nodeStore;
     private ObserverTracker observerTracker;
+    private JournalPropertyServiceTracker journalPropertyServiceTracker = new JournalPropertyServiceTracker();
     private ComponentContext context;
     private Whiteboard whiteboard;
     private long deactivationTimestamp = 0;
@@ -444,6 +445,7 @@ private void registerNodeStore() throws IOException {
                 setCacheSegmentCount(cacheSegmentCount).
                 setCacheStackMoveDistance(cacheStackMoveDistance).
                 setBundlingDisabled(bundlingDisabled).
+                setJournalServicePropertyTracker(journalPropertyServiceTracker).
                 setLeaseCheck(!ClusterNodeInfo.DEFAULT_LEASE_CHECK_DISABLED /* OAK-2739: enabled by default */).
                 setLeaseFailureHandler(new LeaseFailureHandler() {
                     
@@ -573,6 +575,7 @@ public void handleLeaseFailure() {
 
         observerTracker = new ObserverTracker(nodeStore);
         observerTracker.start(context.getBundleContext());
+        journalPropertyServiceTracker.start(whiteboard);
 
         DocumentStore ds = nodeStore.getDocumentStore();
 
@@ -614,6 +617,10 @@ protected void deactivate() {
             observerTracker.stop();
         }
 
+        if (journalPropertyServiceTracker!= null){
+            journalPropertyServiceTracker.stop();
+        }
+
         unregisterNodeStore();
     }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalDiffLoader.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalDiffLoader.java
index f64392f..92e2c7e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalDiffLoader.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalDiffLoader.java
@@ -159,7 +159,7 @@ private void readTrunkChanges(String path,
                 // use revision with a timestamp of zero
                 from = new Revision(0, 0, to.getClusterId());
             }
-            stats.numJournalEntries += fillExternalChanges(changes, path, from, to, ns.getDocumentStore(), null);
+            stats.numJournalEntries += fillExternalChanges(changes, path, from, to, ns.getDocumentStore(), null, null);
         }
         // do we need to include changes from pending local changes?
         if (!max.isRevisionNewer(localLastRev)
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
index 043ebe0..ff56622 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalEntry.java
@@ -40,6 +40,7 @@
 import org.apache.jackrabbit.oak.plugins.document.util.Utils;
 import org.apache.jackrabbit.oak.plugins.observation.ChangeSet;
 import org.apache.jackrabbit.oak.plugins.observation.ChangeSetBuilder;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -84,6 +85,8 @@
 
     private final ChangeSetBuilder changeSetBuilder;
 
+    private final JournalPropertyHandler journalPropertyHandler;
+
     private volatile TreeNode changes = null;
 
     /**
@@ -100,13 +103,15 @@
     private boolean concurrent;
 
     JournalEntry(DocumentStore store) {
-        this(store, false, null);
+        this(store, false, null, null);
     }
 
-    JournalEntry(DocumentStore store, boolean concurrent, ChangeSetBuilder changeSetBuilder) {
+    JournalEntry(DocumentStore store, boolean concurrent, ChangeSetBuilder changeSetBuilder,
+                 JournalPropertyHandler journalPropertyHandler) {
         this.store = store;
         this.concurrent = concurrent;
         this.changeSetBuilder = changeSetBuilder;
+        this.journalPropertyHandler = journalPropertyHandler;
     }
 
     static StringSort newSorter() {
@@ -226,7 +231,7 @@ static int fillExternalChanges(@Nonnull StringSort sorter,
                                    @Nonnull Revision to,
                                    @Nonnull DocumentStore store)
             throws IOException {
-        return fillExternalChanges(sorter, PathUtils.ROOT_PATH, from, to, store, null);
+        return fillExternalChanges(sorter, PathUtils.ROOT_PATH, from, to, store, null, null);
     }
 
     /**
@@ -245,7 +250,9 @@ static int fillExternalChanges(@Nonnull StringSort sorter,
      * @param to     the upper bound of the revision range (inclusive).
      * @param store  the document store to query.
      * @param changeSetBuilder a nullable ChangeSetBuilder to collect changes from
-     *                         the JournalEntry between given revisions     *
+     *                         the JournalEntry between given revisions
+     * @param journalPropertyHandler a nullable JournalPropertyHandler to read
+     *                               stored journal properties for builders from JournalPropertyService
      * @return the number of journal entries read from the store.
      * @throws IOException
      */
@@ -254,7 +261,8 @@ static int fillExternalChanges(@Nonnull StringSort sorter,
                                    @Nonnull Revision from,
                                    @Nonnull Revision to,
                                    @Nonnull DocumentStore store,
-                                   @Nullable ChangeSetBuilder changeSetBuilder)
+                                   @Nullable ChangeSetBuilder changeSetBuilder,
+                                   @Nullable JournalPropertyHandler journalPropertyHandler)
             throws IOException {
         checkNotNull(path);
         checkArgument(checkNotNull(from).getClusterId() == checkNotNull(to).getClusterId());
@@ -292,10 +300,7 @@ static int fillExternalChanges(@Nonnull StringSort sorter,
             }
 
             for (JournalEntry d : partialResult) {
-                d.addTo(sorter, path);
-                if (changeSetBuilder != null) {
-                    d.addTo(changeSetBuilder);
-                }
+                fillFromJournalEntry(sorter, path, changeSetBuilder, journalPropertyHandler, d);
             }
             if (partialResult.size() < READ_CHUNK_SIZE) {
                 break;
@@ -312,13 +317,26 @@ static int fillExternalChanges(@Nonnull StringSort sorter,
                 || (lastEntry != null && !lastEntry.getId().equals(inclusiveToId))) {
             String maxId = asId(new Revision(Long.MAX_VALUE, 0, to.getClusterId()));
             for (JournalEntry d : store.query(JOURNAL, inclusiveToId, maxId, 1)) {
-                d.addTo(sorter, path);
+                fillFromJournalEntry(sorter, path, changeSetBuilder, journalPropertyHandler, d);
                 numEntries++;
             }
         }
         return numEntries;
     }
 
+    private static void fillFromJournalEntry(@Nonnull StringSort sorter, @Nonnull String path,
+                                             @Nullable ChangeSetBuilder changeSetBuilder,
+                                             @Nullable JournalPropertyHandler journalPropertyHandler,
+                                             JournalEntry d) throws IOException {
+        d.addTo(sorter, path);
+        if (changeSetBuilder != null) {
+            d.addTo(changeSetBuilder);
+        }
+        if (journalPropertyHandler != null){
+            journalPropertyHandler.readFrom(d);
+        }
+    }
+
     long getRevisionTimestamp() {
         final String[] parts = getId().split("-");
         return Long.parseLong(parts[1], 16);
@@ -350,6 +368,12 @@ void addChangeSet(@Nullable ChangeSet changeSet){
         changeSetBuilder.add(changeSet);
     }
 
+    public void readFrom(CommitInfo info) {
+        if (journalPropertyHandler != null){
+            journalPropertyHandler.readFrom(info);
+        }
+    }
+
     private void addTo(ChangeSetBuilder changeSetBuilder) {
         String cs = (String) get(CHANGE_SET);
         ChangeSet set = null;
@@ -392,6 +416,9 @@ UpdateOp asUpdateOp(@Nonnull Revision revision) {
         if (changeSetBuilder != null) {
             op.set(CHANGE_SET, changeSetBuilder.build().asString());
         }
+        if (journalPropertyHandler != null){
+            journalPropertyHandler.addTo(op);
+        }
 
         // OAK-3085 : introduce a timestamp property
         // for later being used by OAK-3001
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyHandler.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyHandler.java
new file mode 100644
index 0000000..a44a435
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyHandler.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;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalProperty;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyBuilder;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyService;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+import org.apache.jackrabbit.oak.spi.commit.CommitContext;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+class JournalPropertyHandler {
+    private final Map<String, JournalPropertyBuilder<JournalProperty>> builders = Maps.newHashMap();
+
+    public JournalPropertyHandler(List<JournalPropertyService> services) {
+        for (JournalPropertyService srv : services){
+            builders.put(srv.getName(), srv.newBuilder());
+        }
+    }
+
+    public void readFrom(CommitInfo info){
+        CommitContext commitContext = (CommitContext) info.getInfo().get(CommitContext.NAME);
+
+        //Even if commit content is null do a callback to builder to indicate
+        //that it may miss out on some data collection
+        if (commitContext == null){
+            for (JournalPropertyBuilder<?> builder : builders.values()){
+                builder.addProperty(null);
+            }
+            return;
+        }
+
+        for (Map.Entry<String,JournalPropertyBuilder<JournalProperty>> e : builders.entrySet()){
+            JournalPropertyBuilder<JournalProperty> builder = e.getValue();
+            builder.addProperty(getEntry(commitContext, e.getKey()));
+        }
+    }
+
+    public void readFrom(JournalEntry entry){
+        for (Map.Entry<String,JournalPropertyBuilder<JournalProperty>> e : builders.entrySet()){
+            JournalPropertyBuilder<JournalProperty> builder = e.getValue();
+            String name = Utils.escapePropertyName(e.getKey());
+            builder.addSerializedProperty((String) entry.get(name));
+        }
+    }
+
+    public void addTo(CommitContext commitContext){
+        for (Map.Entry<String,JournalPropertyBuilder<JournalProperty>> e : builders.entrySet()){
+            JournalPropertyBuilder<JournalProperty> builder = e.getValue();
+            commitContext.set(e.getKey(), builder.build());
+        }
+    }
+
+    public void addTo(UpdateOp op){
+        for (Map.Entry<String,JournalPropertyBuilder<JournalProperty>> e : builders.entrySet()){
+            JournalPropertyBuilder<JournalProperty> builder = e.getValue();
+            String name = Utils.escapePropertyName(e.getKey());
+            op.set(name, builder.buildAsString());
+        }
+    }
+
+    private static JournalProperty getEntry(CommitContext cc, String name){
+        Object o = cc.get(name);
+        if (o == null){
+            return null;
+        }
+        checkArgument(o instanceof JournalProperty, "CommitContext entry for name [%s] " +
+                "having value [%s] is not of type JournalEntry", name, o);
+        return (JournalProperty) o;
+    }
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyServiceTracker.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyServiceTracker.java
new file mode 100644
index 0000000..af29fb6
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/JournalPropertyServiceTracker.java
@@ -0,0 +1,38 @@
+/*
+ * 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 org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyService;
+import org.apache.jackrabbit.oak.spi.whiteboard.AbstractServiceTracker;
+
+public class JournalPropertyServiceTracker extends AbstractServiceTracker<JournalPropertyService> {
+
+    public JournalPropertyServiceTracker() {
+        super(JournalPropertyService.class);
+    }
+
+    public JournalPropertyHandler newHandler(){
+        return new JournalPropertyHandler(getServices());
+    }
+
+    public int getServiceCount(){
+        return getServices().size();
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalProperty.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalProperty.java
new file mode 100644
index 0000000..a29309f
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalProperty.java
@@ -0,0 +1,27 @@
+/*
+ * 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.spi;
+
+/**
+ * Marker interface to indicate the implementing class can be made part of JournalEntry
+ */
+public interface JournalProperty {
+
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyBuilder.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyBuilder.java
new file mode 100644
index 0000000..105f7c5
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.spi;
+
+import javax.annotation.Nullable;
+
+public interface JournalPropertyBuilder<T extends JournalProperty> {
+    /**
+     * Adds the JournalProperty instance fetched from CommitInfo to this builder
+     */
+    void addProperty(@Nullable T journalProperty);
+
+    /**
+     * Returns a string representation state of the builder which
+     * would be stored in JournalEntry
+     */
+    String buildAsString();
+
+    /**
+     * Adds the serialized form of journal property (as build from #buildAsString)
+     * call
+     */
+    void addSerializedProperty(@Nullable String serializedProperty);
+
+    /**
+     * Constructs a JournalProperty instance based on current builder state
+     */
+    JournalProperty build();
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyService.java
new file mode 100644
index 0000000..916a60c
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/spi/JournalPropertyService.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.jackrabbit.oak.plugins.document.spi;
+
+/**
+ * Each component which needs to add a property to JournalEntry
+ * should register this service
+ */
+public interface JournalPropertyService {
+
+    JournalPropertyBuilder newBuilder();
+
+    /**
+     * Name of the journal property
+     */
+    String getName();
+
+}
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreServiceTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreServiceTest.java
index 8c77db3..408463d 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreServiceTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreServiceTest.java
@@ -17,11 +17,13 @@
 package org.apache.jackrabbit.oak.plugins.document;
 
 import java.io.File;
+import java.util.Collections;
 import java.util.Map;
 
 import com.google.common.collect.Maps;
 
 import org.apache.commons.io.FilenameUtils;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyService;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.apache.jackrabbit.oak.stats.StatisticsProvider;
 import org.apache.sling.testing.mock.osgi.MockOsgi;
@@ -32,10 +34,12 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
 
 public class DocumentNodeStoreServiceTest {
 
@@ -110,7 +114,16 @@ public void disablePersistentCacheWithRepositoryHome() {
     public void disableJournalCacheWithRepositoryHome() {
         String journalCache = FilenameUtils.concat(repoHome, "diff-cache");
         assertNoJournalCachePath(journalCache, "-", repoHome);
+    }
+
+    @Test
+    public void journalPropertyTracker() throws Exception{
+        MockOsgi.activate(service, context.bundleContext(), Collections.<String,Object>emptyMap());
+        DocumentNodeStore store = context.getService(DocumentNodeStore.class);
+        assertEquals(0, store.getJournalPropertyServiceTracker().getServiceCount());
 
+        context.registerService(JournalPropertyService.class, mock(JournalPropertyService.class));
+        assertEquals(1, store.getJournalPropertyServiceTracker().getServiceCount());
     }
 
     private void assertPersistentCachePath(String expectedPath,
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ExternalChangesTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ExternalChangesTest.java
index 7395b74..4ab4ade 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ExternalChangesTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/ExternalChangesTest.java
@@ -24,12 +24,19 @@
 import java.util.Set;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import org.apache.jackrabbit.oak.core.SimpleCommitContext;
 import org.apache.jackrabbit.oak.plugins.document.memory.MemoryDocumentStore;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalProperty;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyBuilder;
+import org.apache.jackrabbit.oak.plugins.document.spi.JournalPropertyService;
 import org.apache.jackrabbit.oak.plugins.observation.ChangeCollectorProvider;
 import org.apache.jackrabbit.oak.plugins.observation.ChangeSet;
 import org.apache.jackrabbit.oak.spi.commit.CommitContext;
@@ -40,6 +47,8 @@
 import org.apache.jackrabbit.oak.spi.commit.Observer;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -61,9 +70,12 @@
 
     private CommitInfoCollector c1 = new CommitInfoCollector();
     private CommitInfoCollector c2 = new CommitInfoCollector();
+    private JournalPropertyServiceTracker tracker = new JournalPropertyServiceTracker();
+    private Whiteboard wb = new DefaultWhiteboard();
 
     @Before
     public void setUp() {
+        tracker.start(wb);
         MemoryDocumentStore store = new MemoryDocumentStore();
         ns1 = newDocumentNodeStore(store, 1);
         ns2 = newDocumentNodeStore(store, 2);
@@ -163,12 +175,58 @@ public void changeSetForBranchCommit() throws Exception{
         assertTrue(cs.getPropertyNames().containsAll(propNames));
     }
 
+    @Test
+    public void journalService() throws Exception{
+        wb.register(JournalPropertyService.class, new TestJournalService(), null);
+
+        //Do a dummy write so that journal property handler gets refreshed
+        //and picks our newly registered service
+        NodeBuilder b0 = ns1.getRoot().builder();
+        b0.child("0");
+        ns1.merge(b0, newCollectingHook(), newCommitInfo());
+        ns1.backgroundWrite();
+
+        NodeBuilder b1 = ns1.getRoot().builder();
+        b1.child("a");
+        CommitContext cc = new SimpleCommitContext();
+        cc.set(TestProperty.NAME, new TestProperty("foo"));
+        ns1.merge(b1, newCollectingHook(), newCommitInfo(cc));
+
+        NodeBuilder b2 = ns1.getRoot().builder();
+        b2.child("b");
+        cc = new SimpleCommitContext();
+        cc.set(TestProperty.NAME, new TestProperty("bar"));
+        ns1.merge(b2, newCollectingHook(), newCommitInfo(cc));
+
+        //null entry
+        NodeBuilder b3 = ns1.getRoot().builder();
+        b3.child("c");
+        ns1.merge(b3, newCollectingHook(), newCommitInfo());
+
+        ns1.backgroundWrite();
+
+        c2.reset();
+        ns2.backgroundRead();
+
+        CommitInfo ci = c2.getExternalChange();
+        cc = (CommitContext) ci.getInfo().get(CommitContext.NAME);
+
+        CumulativeTestProperty ct = (CumulativeTestProperty) cc.get(TestProperty.NAME);
+        assertNotNull(ct);
+
+        assertThat(ct.values, containsInAnyOrder("foo", "bar", "NULL"));
+    }
+
     private CommitHook newCollectingHook(){
         return new EditorHook(new ChangeCollectorProvider());
     }
 
     private CommitInfo newCommitInfo(){
-        Map<String, Object> info = ImmutableMap.<String, Object>of(CommitContext.NAME, new SimpleCommitContext());
+        return newCommitInfo(new SimpleCommitContext());
+    }
+
+    private CommitInfo newCommitInfo(CommitContext commitContext){
+        Map<String, Object> info = ImmutableMap.<String, Object>of(CommitContext.NAME, commitContext);
         return new CommitInfo(CommitInfo.OAK_UNKNOWN, CommitInfo.OAK_UNKNOWN, info);
     }
 
@@ -176,6 +234,7 @@ private DocumentNodeStore newDocumentNodeStore(DocumentStore store, int clusterI
         return builderProvider.newBuilder()
                 .setAsyncDelay(0)
                 .setDocumentStore(store)
+                .setJournalServicePropertyTracker(tracker)
                 .setLeaseCheck(false) // disabled for debugging purposes
                 .setClusterId(clusterId)
                 .getNodeStore();
@@ -204,4 +263,60 @@ void reset(){
             infos.clear();
         }
     }
+
+    private static class TestJournalService implements JournalPropertyService {
+
+        @Override
+        public JournalPropertyBuilder newBuilder() {
+            return new TestJournalBuilder();
+        }
+
+        @Override
+        public String getName() {
+            return TestProperty.NAME;
+        }
+    }
+
+    private static class TestProperty implements JournalProperty {
+        static final String NAME = "test.props";
+        final String value;
+
+        public TestProperty(String value) {
+            this.value = value;
+        }
+    }
+
+    private static class CumulativeTestProperty implements JournalProperty {
+        final Set<String> values = Sets.newHashSet();
+    }
+
+    private static class TestJournalBuilder implements JournalPropertyBuilder<TestProperty>{
+        final CumulativeTestProperty allProps = new CumulativeTestProperty();
+
+        @Override
+        public void addProperty(@Nullable TestProperty journalProperty) {
+            if (journalProperty != null) {
+                allProps.values.add(journalProperty.value);
+            } else {
+                allProps.values.add("NULL");
+            }
+        }
+
+        @Override
+        public String buildAsString() {
+            return Joiner.on(",").join(allProps.values);
+        }
+
+        @Override
+        public void addSerializedProperty(@Nullable String s) {
+            if (s != null){
+                Iterables.addAll(allProps.values, Splitter.on(',').split(s));
+            }
+        }
+
+        @Override
+        public JournalProperty build() {
+            return allProps;
+        }
+    }
 }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalEntryTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalEntryTest.java
index 6495e61..9b1a5a9 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalEntryTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/JournalEntryTest.java
@@ -249,7 +249,7 @@ public void fillExternalChangesWithPath() throws Exception {
         assertTrue(store.create(JOURNAL, Collections.singletonList(op)));
 
         StringSort sort = JournalEntry.newSorter();
-        JournalEntry.fillExternalChanges(sort, "/foo", r1, r2, store, null);
+        JournalEntry.fillExternalChanges(sort, "/foo", r1, r2, store, null, null);
         assertEquals(4, sort.getSize());
         sort.close();
     }
