You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2023/12/11 10:37:41 UTC

(causeway) 23/25: Implement transaction-scoping and add associated changes

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-3654-isis
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 9eb4bbf8bd8ef2a817bb3dddf6f8f7da45656760
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Dec 10 13:36:00 2023 +0000

    Implement transaction-scoping and add associated changes
    
    Introduces transaction-scoping being governed by the new @TransactionScope annotation.
    
    This makes way for the replacement of interaction-scoping of EntityChangeTrackerDefault with the newly introduced transaction-scoping.
    
    The now redundant TransactionEventEmitter class is removed, signifying the complete migration to transaction-scoping.
---
 .editorconfig                                      |  2 +-
 .../isis/applib/annotation/InteractionScope.java   | 14 +++-
 .../isis/applib/annotation/TransactionScope.java   | 77 ++++++++++++++++++++++
 .../interaction/IsisModuleCoreInteraction.java     |  3 +
 .../InteractionScopeBeanFactoryPostProcessor.java  |  2 +-
 ... TransactionScopeBeanFactoryPostProcessor.java} | 12 ++--
 .../isis/core/runtime/IsisModuleCoreRuntime.java   |  2 -
 .../runtime/events/TransactionEventEmitter.java    | 66 -------------------
 .../org/apache/isis/core/runtime/events/_Xray.java |  2 +-
 .../publish/ObjectLifecyclePublisherDefault.java   |  3 +
 .../transaction/TransactionServiceSpring.java      | 73 ++++++++++++--------
 core/transaction/src/main/java/module-info.java    |  1 +
 .../events/TransactionCompletionStatus.java        | 76 +++++++++++++++++++++
 persistence/commons/pom.xml                        |  5 ++
 persistence/commons/src/main/java/module-info.java |  1 +
 .../changetracking/EntityChangeTrackerDefault.java | 19 +++---
 16 files changed, 243 insertions(+), 115 deletions(-)

diff --git a/.editorconfig b/.editorconfig
index f7644a1dfb..a806c86944 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -112,7 +112,7 @@ ij_java_for_statement_wrap = off
 ij_java_generate_final_locals = false
 ij_java_generate_final_parameters = false
 ij_java_if_brace_force = never
-ij_java_imports_layout = java.**,|,$java.**,|,javax.**,|,$javax.**,|,com.**,|,$com.**,|,org.**,|,$org.**,|,org.springframework.**,|,$org.springframework.**,|,org.apache.causeway.**,|,$org.apache.causeway.**,|,lombok.**,lombok.experimental.**,lombok.extern.log4j.**,lombok.extern.**,|,demoapp.**,|,*,|,$*
+ij_java_imports_layout = java.**,|,$java.**,|,javax.**,|,$javax.**,|,com.**,|,$com.**,|,org.**,|,$org.**,|,org.springframework.**,|,$org.springframework.**,|,org.apache.causeway.**,|,$org.apache.causeway.**,|,org.apache.isis.**,|,$org.apache.isis.**,|,lombok.**,lombok.experimental.**,lombok.extern.log4j.**,lombok.extern.**,|,demoapp.**,|,*,|,$*
 ij_java_indent_case_from_switch = true
 ij_java_insert_inner_class_imports = false
 ij_java_insert_override_annotation = true
diff --git a/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
index a8b2632d12..b8e1401f5b 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/InteractionScope.java
@@ -25,6 +25,8 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 import org.springframework.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.core.annotation.AliasFor;
 
 /**
  * {@code @InteractionScope} is a specialization of {@link Scope @Scope} for a
@@ -40,6 +42,7 @@ import org.springframework.context.annotation.Scope;
  * @since 2.0 {@index}
  * @see org.springframework.web.context.annotation.SessionScope
  * @see org.springframework.web.context.annotation.ApplicationScope
+ * @see TransactionScope
  * @see org.springframework.context.annotation.Scope
  * @see org.springframework.stereotype.Component
  * @see org.springframework.context.annotation.Bean
@@ -47,7 +50,16 @@ import org.springframework.context.annotation.Scope;
 @Target({ElementType.TYPE, ElementType.METHOD})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-@Scope("interaction")
+@Scope(InteractionScope.SCOPE_NAME)
 public @interface InteractionScope {
 
+    String SCOPE_NAME = "interaction";
+
+    /**
+     * Alias for {@link Scope#proxyMode}.
+     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
+     */
+    @AliasFor(annotation = Scope.class)
+    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
 }
diff --git a/api/applib/src/main/java/org/apache/isis/applib/annotation/TransactionScope.java b/api/applib/src/main/java/org/apache/isis/applib/annotation/TransactionScope.java
new file mode 100644
index 0000000000..a3177eca71
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/isis/applib/annotation/TransactionScope.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.isis.applib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * {@code @TransactionScope} is a specialization of {@link Scope @Scope} for a
+ * service or component whose lifecycle is bound to the current top-level transaction,
+ * within an outer {@link InteractionScope interaction}.
+ *
+ * <p>Such services should additional implement Spring's
+ * {@link org.springframework.transaction.support.TransactionSynchronization} interface, defining the transaction
+ * lifecycle callbacks.
+ *
+ * <p>Specifically, {@code @TransactionScope} is a <em>composed annotation</em> that
+ * acts as a shortcut for {@code @Scope("transaction")}.
+ *
+ * <p>{@code @TransactionScope} may be used as a meta-annotation to create custom
+ * composed annotations.
+ *
+ * <p> Note that (apparently) the {@link org.springframework.transaction.support.TransactionSynchronization}
+ * infrastructure is only really intended to work with a single {@link org.springframework.transaction.PlatformTransactionManager}.
+ * And indeed, this is going to be typical case.  However, our framework code does at least admit the possibility of
+ * multiple {@link org.springframework.transaction.PlatformTransactionManager}s being defined in the app.  If that is
+ * the case, then (I believe) the callbacks of {@link org.springframework.transaction.support.TransactionSynchronization} might
+ * be called multiple times, once per {@link org.springframework.transaction.PlatformTransactionManager}.  The framework
+ * currently doesn't provide any way to distinguish between these calls.
+ *
+ * @since 2.0 {@index}
+ * @see org.springframework.web.context.annotation.SessionScope
+ * @see org.springframework.web.context.annotation.ApplicationScope
+ * @see InteractionScope
+ * @see Scope
+ * @see org.springframework.stereotype.Component
+ * @see org.springframework.context.annotation.Bean
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Scope(TransactionScope.SCOPE_NAME)
+public @interface TransactionScope {
+
+    String SCOPE_NAME = "transaction";
+
+    /**
+     * Alias for {@link Scope#proxyMode}.
+     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
+     */
+    @AliasFor(annotation = Scope.class)
+    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
+}
diff --git a/core/interaction/src/main/java/org/apache/isis/core/interaction/IsisModuleCoreInteraction.java b/core/interaction/src/main/java/org/apache/isis/core/interaction/IsisModuleCoreInteraction.java
index 98748f3563..d8c269b1bd 100644
--- a/core/interaction/src/main/java/org/apache/isis/core/interaction/IsisModuleCoreInteraction.java
+++ b/core/interaction/src/main/java/org/apache/isis/core/interaction/IsisModuleCoreInteraction.java
@@ -18,6 +18,8 @@
  */
 package org.apache.isis.core.interaction;
 
+import org.apache.isis.core.interaction.scope.TransactionScopeBeanFactoryPostProcessor;
+
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
@@ -27,6 +29,7 @@ import org.apache.isis.core.interaction.scope.InteractionScopeBeanFactoryPostPro
 @Import({
 
     InteractionScopeBeanFactoryPostProcessor.class,
+    TransactionScopeBeanFactoryPostProcessor.class,
 
 })
 public class IsisModuleCoreInteraction {
diff --git a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
index 8627359f42..57ef476adf 100644
--- a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
+++ b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
@@ -32,7 +32,7 @@ import lombok.val;
 @Component
 public class InteractionScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
 
-    public static final String SCOPE_NAME = "interaction";
+    public static final String SCOPE_NAME = org.apache.isis.applib.annotation.InteractionScope.SCOPE_NAME;
 
     @Override
     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
diff --git a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
similarity index 74%
copy from core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
copy to core/interaction/src/main/java/org/apache/isis/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
index 8627359f42..d4e6b4b9af 100644
--- a/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
+++ b/core/interaction/src/main/java/org/apache/isis/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
@@ -23,6 +23,7 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
 import org.springframework.beans.factory.config.ConfigurableBeanFactory;
 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.SimpleTransactionScope;
 
 import lombok.val;
 
@@ -30,17 +31,14 @@ import lombok.val;
  * @since 2.0
  */
 @Component
-public class InteractionScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
+public class TransactionScopeBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
 
-    public static final String SCOPE_NAME = "interaction";
+    public static final String SCOPE_NAME = org.apache.isis.applib.annotation.TransactionScope.SCOPE_NAME;
 
     @Override
     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
-        val interactionScope = new InteractionScope(beanFactory);
-        beanFactory.registerScope(SCOPE_NAME, interactionScope);
+        val transactionScope = new SimpleTransactionScope();
+        beanFactory.registerScope(SCOPE_NAME, transactionScope);
     }
 
-    public static InteractionScopeLifecycleHandler lookupScope(ConfigurableBeanFactory beanFactory) throws BeansException {
-        return (InteractionScopeLifecycleHandler) beanFactory.getRegisteredScope(SCOPE_NAME);
-    }
 }
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
index b845cf1b72..e515605b86 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
@@ -24,7 +24,6 @@ import org.springframework.context.annotation.Import;
 import org.apache.isis.core.interaction.IsisModuleCoreInteraction;
 import org.apache.isis.core.metamodel.IsisModuleCoreMetamodel;
 import org.apache.isis.core.runtime.events.MetamodelEventService;
-import org.apache.isis.core.runtime.events.TransactionEventEmitter;
 import org.apache.isis.core.transaction.IsisModuleCoreTransaction;
 import org.apache.isis.valuetypes.jodatime.integration.IsisModuleValJodatimeIntegration;
 
@@ -40,7 +39,6 @@ import org.apache.isis.valuetypes.jodatime.integration.IsisModuleValJodatimeInte
 
         // @Service's
         MetamodelEventService.class,
-        TransactionEventEmitter.class,
 
         // @Configuration's
 
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/events/TransactionEventEmitter.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/events/TransactionEventEmitter.java
deleted file mode 100644
index 2a50514f5b..0000000000
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/events/TransactionEventEmitter.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- *  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.isis.core.runtime.events;
-
-import javax.inject.Inject;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.support.TransactionSynchronization;
-import org.springframework.transaction.support.TransactionSynchronizationManager;
-
-import org.apache.isis.applib.services.eventbus.EventBusService;
-import org.apache.isis.applib.services.iactnlayer.InteractionLayerTracker;
-import org.apache.isis.core.interaction.scope.TransactionBoundaryAware;
-import org.apache.isis.core.transaction.events.TransactionAfterCompletionEvent;
-import org.apache.isis.core.transaction.events.TransactionBeforeCompletionEvent;
-
-import lombok.RequiredArgsConstructor;
-import lombok.val;
-
-@Service
-@RequiredArgsConstructor(onConstructor_ = {@Inject})
-public class TransactionEventEmitter
-implements TransactionSynchronization, TransactionBoundaryAware {
-
-    private final EventBusService eventBusService;
-    private final InteractionLayerTracker interactionLayerTracker;
-
-    @Override
-    public void beforeCompletion() {
-        _Xray.txBeforeCompletion(interactionLayerTracker, "tx: beforeCompletion");
-        eventBusService.post(TransactionBeforeCompletionEvent.instance());
-    }
-
-    @Override
-    public void afterCompletion(final int status) {
-        val event = TransactionAfterCompletionEvent.forStatus(status);
-        eventBusService.post(event);
-        _Xray.txAfterCompletion(interactionLayerTracker, String.format("tx: afterCompletion (%s)", event.name()));
-    }
-
-    @Override
-    public void afterEnteringTransactionalBoundary(PlatformTransactionManager platformTransactionManager) {
-        val isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
-        if(isSynchronizationActive) {
-            TransactionSynchronizationManager.registerSynchronization(this);
-        }
-    }
-
-}
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/events/_Xray.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/events/_Xray.java
index 30a5801fb3..04833901de 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/events/_Xray.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/events/_Xray.java
@@ -29,7 +29,7 @@ import org.apache.isis.core.security.util.XrayUtil;
 
 import lombok.val;
 
-final class _Xray {
+public final class _Xray {
 
     static void addConfiguration(final ConfigurationViewService configurationService) {
 
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
index abba0cad05..25bfb4919e 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
@@ -26,6 +26,8 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import org.apache.isis.applib.annotation.TransactionScope;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
@@ -47,6 +49,7 @@ import lombok.RequiredArgsConstructor;
  * @since 2.0 {@index}
  */
 @Service
+@TransactionScope
 @Named(IsisModuleCoreRuntimeServices.NAMESPACE + ".ObjectLifecyclePublisherDefault")
 @Priority(PriorityPrecedence.EARLY)
 @Qualifier("Default")
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/transaction/TransactionServiceSpring.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/transaction/TransactionServiceSpring.java
index 74a8c5ce63..5c1ee193ce 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/transaction/TransactionServiceSpring.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/transaction/TransactionServiceSpring.java
@@ -33,7 +33,12 @@ import org.apache.isis.commons.internal.debug._Probe;
 import org.apache.isis.core.interaction.scope.TransactionBoundaryAware;
 import org.apache.isis.core.interaction.session.IsisInteraction;
 
+import org.apache.isis.core.runtime.events._Xray;
+import org.apache.isis.core.transaction.events.TransactionCompletionStatus;
+
+import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.context.event.EventListener;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
@@ -41,6 +46,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.PlatformTransactionManager;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.transaction.support.TransactionTemplate;
 
@@ -82,25 +88,30 @@ implements
     private final Can<PlatformTransactionManager> platformTransactionManagers;
     private final InteractionLayerTracker interactionLayerTracker;
     private final Can<PersistenceExceptionTranslator> persistenceExceptionTranslators;
-    private final Can<TransactionBoundaryAware> transactionBoundaryAwareBeans;
+    private final ConfigurableListableBeanFactory configurableListableBeanFactory;
+//    private final Can<TransactionBoundaryAware> transactionBoundaryAwareBeans;
 
     @Inject
     public TransactionServiceSpring(
             final List<PlatformTransactionManager> platformTransactionManagers,
             final List<PersistenceExceptionTranslator> persistenceExceptionTranslators,
             final InteractionLayerTracker interactionLayerTracker,
-            final List<TransactionBoundaryAware> transactionBoundaryAwareBeans) {
+            final ConfigurableListableBeanFactory configurableListableBeanFactory
+//            , final List<TransactionBoundaryAware> transactionBoundaryAwareBeans
+    ) {
 
         this.platformTransactionManagers = Can.ofCollection(platformTransactionManagers);
         log.info("PlatformTransactionManagers: {}", platformTransactionManagers);
 
+        this.configurableListableBeanFactory = configurableListableBeanFactory;
+
         this.persistenceExceptionTranslators = Can.ofCollection(persistenceExceptionTranslators);
         log.info("PersistenceExceptionTranslators: {}", persistenceExceptionTranslators);
 
         this.interactionLayerTracker = interactionLayerTracker;
 
-        this.transactionBoundaryAwareBeans = Can.ofCollection(transactionBoundaryAwareBeans);
-        log.info("TransactionBoundaryAwareBeans: {}", transactionBoundaryAwareBeans);
+//        this.transactionBoundaryAwareBeans = Can.ofCollection(transactionBoundaryAwareBeans);
+//        log.info("TransactionBoundaryAwareBeans: {}", transactionBoundaryAwareBeans);
     }
 
     // -- SPRING INTEGRATION
@@ -115,24 +126,30 @@ implements
         try {
 
             TransactionStatus tx = platformTransactionManager.getTransaction(def);
-            if(tx.isNewTransaction()) {
-                transactionBoundaryAwareBeans.forEach(tba -> tba.afterEnteringTransactionalBoundary(platformTransactionManager));
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> tba.afterEnteringTransactionalBoundary(platformTransactionManager));
+//            }
+            if (TransactionSynchronizationManager.isSynchronizationActive()) {
+                configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
+                        .values()
+                        .forEach(TransactionSynchronizationManager::registerSynchronization);
             }
 
+
             result = Try.call(callable)
                     .mapFailure(ex->translateExceptionIfPossible(ex, platformTransactionManager));
 
-            if(tx.isNewTransaction()) {
-                transactionBoundaryAwareBeans.forEach(tba -> tba.beforeLeavingTransactionalBoundary(platformTransactionManager));
-            }
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> tba.beforeLeavingTransactionalBoundary(platformTransactionManager));
+//            }
             if(result.isFailure()) {
                 platformTransactionManager.rollback(tx);
             } else {
                 platformTransactionManager.commit(tx);
             }
-            if(tx.isNewTransaction()) {
-                transactionBoundaryAwareBeans.forEach(tba -> tba.afterLeavingTransactionalBoundary(platformTransactionManager));
-            }
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> tba.afterLeavingTransactionalBoundary(platformTransactionManager));
+//            }
         } catch (Exception ex) {
 
             return result!=null
@@ -254,17 +271,6 @@ implements
 
     private ThreadLocal<LongAdder> txCounter = ThreadLocal.withInitial(LongAdder::new);
 
-    /** TRANSACTION END BOUNDARY */
-
-    /**
-     * This is called by TransactionEventEmitter, which is a TransactionBoundaryAware bean.
-     *
-     * @param event
-     */
-    @EventListener(TransactionAfterCompletionEvent.class)
-    public void onTransactionEnded(final TransactionAfterCompletionEvent event) {
-        txCounter.get().increment();
-    }
 
     // -- HELPER
 
@@ -356,12 +362,19 @@ implements
                 // either participate in existing or create new transaction
                 TransactionStatus txStatus = txManager.getTransaction(def);
 
-                if(txStatus==null // in support of JUnit testing (TransactionManagers might be mocked or hollow stubs)
-                        || !txStatus.isNewTransaction()) {
+                if (TransactionSynchronizationManager.isSynchronizationActive()) {
+                    configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
+                            .values()
+                            .forEach(TransactionSynchronizationManager::registerSynchronization);
+                }
+
+                if(
+//                        txStatus==null || // in support of JUnit testing (TransactionManagers might be mocked or hollow stubs)
+                    !txStatus.isNewTransaction()) {
                     // we are participating in an exiting transaction (or testing), nothing to do
                     return;
                 }
-                transactionBoundaryAwareBeans.forEach(tbab -> tbab.afterEnteringTransactionalBoundary(txManager));
+//                transactionBoundaryAwareBeans.forEach(tbab -> tbab.afterEnteringTransactionalBoundary(txManager));
 
                 // we have created a new transaction, so need to provide a CloseTask
                 onCloseTasks.add(
@@ -369,13 +382,17 @@ implements
                             txStatus,
                             txManager.getClass().getName(), // info to be used for display in case of errors
                             () -> {
-                                transactionBoundaryAwareBeans.forEach(tbab -> tbab.beforeLeavingTransactionalBoundary(txManager));
+//                                transactionBoundaryAwareBeans.forEach(tbab -> tbab.beforeLeavingTransactionalBoundary(txManager));
+                                _Xray.txBeforeCompletion(interactionLayerTracker, "tx: beforeCompletion");
                                 if (txStatus.isRollbackOnly()) {
                                     txManager.rollback(txStatus);
                                 } else {
                                     txManager.commit(txStatus);
+                                    val event = TransactionCompletionStatus.COMMITTED;
+                                    _Xray.txAfterCompletion(interactionLayerTracker, String.format("tx: afterCompletion (%s)", event.name()));
                                 }
-                                transactionBoundaryAwareBeans.forEach(tbab -> tbab.afterLeavingTransactionalBoundary(txManager));
+//                                transactionBoundaryAwareBeans.forEach(tbab -> tbab.afterLeavingTransactionalBoundary(txManager));
+                                txCounter.get().increment();
                             }
                         )
                 );
diff --git a/core/transaction/src/main/java/module-info.java b/core/transaction/src/main/java/module-info.java
index fa1c303f22..e205ff8528 100644
--- a/core/transaction/src/main/java/module-info.java
+++ b/core/transaction/src/main/java/module-info.java
@@ -34,4 +34,5 @@ module org.apache.isis.core.transaction {
     requires spring.context;
     requires spring.core;
     requires spring.tx;
+    requires java.transaction;
 }
diff --git a/core/transaction/src/main/java/org/apache/isis/core/transaction/events/TransactionCompletionStatus.java b/core/transaction/src/main/java/org/apache/isis/core/transaction/events/TransactionCompletionStatus.java
new file mode 100644
index 0000000000..1ac9ecb05a
--- /dev/null
+++ b/core/transaction/src/main/java/org/apache/isis/core/transaction/events/TransactionCompletionStatus.java
@@ -0,0 +1,76 @@
+/*
+ *  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.isis.core.transaction.events;
+
+import javax.transaction.Status;
+
+/**
+ * @since 2.0 {@index}
+ * @see Status
+ */
+public enum TransactionCompletionStatus {
+
+    /** Completion status in case of proper commit. */
+    COMMITTED,
+
+    /** Completion status in case of proper rollback. */
+    ROLLED_BACK,
+
+    /** Completion status in case of heuristic mixed completion or system errors. */
+    UNKNOWN,
+    ;
+
+    /**
+     * @param status field from {@link Status}.
+     * @return
+     */
+    public static TransactionCompletionStatus forStatus(int status) {
+        switch (status) {
+            case 3:
+                // int STATUS_COMMITTED = 3;
+                return COMMITTED;
+            case 4:
+                // int STATUS_ROLLEDBACK = 4;
+                return ROLLED_BACK;
+            default:
+                // int STATUS_ACTIVE = 0;
+                // int STATUS_MARKED_ROLLBACK = 1;
+                // int STATUS_PREPARED = 2;
+                // int STATUS_UNKNOWN = 5;
+                // int STATUS_NO_TRANSACTION = 6;
+                // int STATUS_PREPARING = 7;
+                // int STATUS_COMMITTING = 8;
+                // int STATUS_ROLLING_BACK = 9;
+                return UNKNOWN;
+        }
+    }
+
+    public boolean isCommitted() {
+        return this == COMMITTED;
+    }
+
+    public boolean isRolledBack() {
+        return this == ROLLED_BACK;
+    }
+
+    public boolean isUnknown() {
+        return this == UNKNOWN;
+    }
+
+}
diff --git a/persistence/commons/pom.xml b/persistence/commons/pom.xml
index 5b8d732e6e..a5adfbe56c 100644
--- a/persistence/commons/pom.xml
+++ b/persistence/commons/pom.xml
@@ -58,6 +58,11 @@
 			<artifactId>isis-core-runtime</artifactId>
 		</dependency>
 
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+        </dependency>
+
 		<!-- TESTING -->
 
         <dependency>
diff --git a/persistence/commons/src/main/java/module-info.java b/persistence/commons/src/main/java/module-info.java
index 90363f6adf..f9ee74fa63 100644
--- a/persistence/commons/src/main/java/module-info.java
+++ b/persistence/commons/src/main/java/module-info.java
@@ -39,4 +39,5 @@ module org.apache.isis.persistence.commons {
     requires spring.beans;
     requires spring.context;
     requires spring.core;
+    requires spring.tx;
 }
diff --git a/persistence/commons/src/main/java/org/apache/isis/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java b/persistence/commons/src/main/java/org/apache/isis/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
index 532a31b04a..d0704d673d 100644
--- a/persistence/commons/src/main/java/org/apache/isis/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
+++ b/persistence/commons/src/main/java/org/apache/isis/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
@@ -32,12 +32,17 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import org.apache.isis.applib.annotation.TransactionScope;
+
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.context.annotation.Scope;
 import org.springframework.context.event.EventListener;
 import org.springframework.core.annotation.Order;
 import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionSynchronization;
 
 import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.EntityChangeKind;
@@ -78,24 +83,22 @@ import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
 /**
- * This service keeps track of all of the changes within a transactoin, for entities for which entity property change
+ * This object keeps track of all of the changes within a transaction, for entities for which entity property change
  * publishing is enabled (typically using the
  * {@link DomainObject#entityChangePublishing() @DomainObject(entityChangePublishing=)} annotation attribute.
  *
  * <p>
- * The service is {@link InteractionScope}d.  In theory this could happen multiple times per interaction, so the
- * data structures are cleared on each commit for potential reuse within the same interaction.  (Of course, because the
- * service <i>is</i> interaction-scoped, a new instance of the service is created for each interaction, and so the
- * data held in this service is private to each user's interaction.
+ * The service is transaction-scoped and implements Spring's TransactionSynchronization interface, meaning that
+ * Spring is responsible for calling it thereafter.
  * </p>
  *
  * @since 2.0 {@index}
  */
 @Service
+@TransactionScope
 @Named("isis.persistence.commons.EntityChangeTrackerDefault")
 @Priority(PriorityPrecedence.EARLY)
 @Qualifier("default")
-@InteractionScope   // see note above regarding this
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
 @Log4j2
 public class EntityChangeTrackerDefault
@@ -103,8 +106,8 @@ implements
     MetricsService,
     EntityChangeTracker,
     HasEnlistedEntityPropertyChanges,
-    HasEnlistedEntityChanges {
-
+    HasEnlistedEntityChanges,
+    TransactionSynchronization {
 
     private final EntityPropertyChangePublisher entityPropertyChangePublisher;
     private final EntityChangesPublisher entityChangesPublisher;