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

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

commit 2755d74bf7b80c138caa5e75f0f76fac431f2818
Author: danhaywood <d...@haywood-associates.co.uk>
AuthorDate: Mon Dec 11 15:49:39 2023 +0000

    CAUSEWAY-3564: introduces TransactionScope annotation and ...
    
    ... reworks InteractionServiceDefault and TransactionServiceSpring to use 
more of Spring's inbuilt xactn support
    Specifically, using TransactionSynchronizationManager and 
TransactionSynchronization to allow suspend/resume
    (meaning REQUIRES_NEW is supported).
    
    In particular, the InteractionAwareTransactionBoundaryHandler service is 
removed, its functionality moved into
    TransactionServiceSpring, and also removed TransactionBoundaryAware 
interface and callbacks - instead,
    TransactionSynchronization can be used.
---
 .editorconfig                                      |   2 +-
 ...ntentNegotiationServiceOrgApacheCausewayV1.adoc |  19 ++
 ...tionServiceOrgApacheCausewayV1_usage-notes.adoc |   6 +
 antora/supplemental-ui/causeway-favicon.png        | Bin 0 -> 2143 bytes
 .../applib/annotation/InteractionScope.java        |  14 +-
 .../applib/annotation/TransactionScope.java        |  79 +++++
 .../services/xactn/TransactionalProcessor.java     |  17 +-
 core/interaction/src/main/java/module-info.java    |   1 -
 .../interaction/CausewayModuleCoreInteraction.java |   4 +-
 ...teractionAwareTransactionalBoundaryHandler.java | 175 ----------
 .../InteractionScopeBeanFactoryPostProcessor.java  |   2 +-
 .../interaction/scope/StackedTransactionScope.java | 184 ++++++++++
 .../TransactionScopeBeanFactoryPostProcessor.java} |  24 +-
 .../metamodel/objectmanager/ObjectCreator.java     |   4 +-
 .../objectlifecycle/ObjectLifecyclePublisher.java  |   4 +
 .../specloader/specimpl/IntrospectionState.java}   |  48 +--
 .../core/runtime/CausewayModuleCoreRuntime.java    |   2 -
 .../runtime/events/TransactionEventEmitter.java    |  67 ----
 .../apache/causeway/core/runtime/events/_Xray.java |   2 +-
 .../runtimeservices/src/main/java/module-info.java |   3 +-
 .../EntityPropertyChangePublisherDefault.java      |   4 +-
 .../publish/ObjectLifecyclePublisherDefault.java   |   2 +
 .../session/InteractionServiceDefault.java         |  38 +--
 .../transaction/TransactionServiceSpring.java      | 218 ++++++++++--
 core/transaction/src/main/java/module-info.java    |   3 +-
 ...Event.java => TransactionCompletionStatus.java} |  31 +-
 .../integtests/OutboxRestClient_E2eTest.java       |   6 +-
 .../spiimpl/SessionSubscriberForSessionLog.java    |  19 +-
 persistence/commons/src/main/java/module-info.java |   3 +-
 .../commons/CausewayModulePersistenceCommons.java  |   1 -
 .../changetracking/EntityChangeTrackerDefault.java | 118 ++-----
 .../CausewayModulePersistenceJdoDatanucleus.java   |   1 +
 .../metamodel/facets/entity/JdoEntityFacet.java    |   1 +
 ...actionRollbackTest_usingInteractionService.java |   2 -
 ...actionRollbackTest_usingTransactionService.java |   2 -
 .../testdomain/conf/Configuration_headless.java    |   1 -
 .../publishing/PublishingTestFactoryAbstract.java  |   2 -
 .../util/interaction/InteractionBoundaryProbe.java |   3 -
 .../rename-all-published-sources-to-causeway.jsh   | 380 +++++++++++++++++++++
 .../branding/BrandingUiServiceDefault.java         |   2 -
 ...r.restfulobjects.applib.client.UriBuilderPlugin |   1 -
 ...ntentNegotiationServiceOrgApacheCausewayV1.java | 108 ++++++
 .../viewer/src/main/java/module-info.java          |   3 +-
 43 files changed, 1137 insertions(+), 469 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/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc
 
b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc
new file mode 100644
index 0000000000..6965204c15
--- /dev/null
+++ 
b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.adoc
@@ -0,0 +1,19 @@
+= ContentNegotiationServiceOrgApacheIsisV1
+:Notice: 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 ag [...]
+
+== API
+
+[source,java]
+.ContentNegotiationServiceOrgApacheIsisV1.java
+----
+class ContentNegotiationServiceOrgApacheIsisV1 {
+  public static final String ACCEPT_PROFILE;
+  Response.ResponseBuilder buildResponse(IResourceContext resourceContext, 
ManagedObject objectAdapter)
+  Response.ResponseBuilder buildResponse(IResourceContext resourceContext, 
ManagedProperty objectAndProperty)
+  Response.ResponseBuilder buildResponse(IResourceContext resourceContext, 
ManagedCollection managedCollection)
+  Response.ResponseBuilder buildResponse(IResourceContext resourceContext, 
ManagedAction objectAndAction)
+  Response.ResponseBuilder buildResponse(IResourceContext resourceContext, 
ObjectAndActionInvocation objectAndActionInvocation)
+}
+----
+
+include::hooks/ContentNegotiationServiceOrgApacheIsisV1_usage-notes.adoc[]
diff --git 
a/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc
 
b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc
new file mode 100644
index 0000000000..5dd9b62ebc
--- /dev/null
+++ 
b/antora/components/refguide-index/modules/viewer/pages/index/restfulobjects/rendering/service/conneg/hooks/ContentNegotiationServiceOrgApacheCausewayV1_usage-notes.adoc
@@ -0,0 +1,6 @@
+:Notice: 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 ag [...]
+
+
+This service returns 
xref:vro:ROOT:content-negotiation/apache-causeway-v2-profile.adoc[simplified 
representations]
+
+
diff --git a/antora/supplemental-ui/causeway-favicon.png 
b/antora/supplemental-ui/causeway-favicon.png
new file mode 100644
index 0000000000..258658996e
Binary files /dev/null and b/antora/supplemental-ui/causeway-favicon.png differ
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java
index 875f16a2ae..894b347fa0 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/annotation/InteractionScope.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/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/causeway/applib/annotation/TransactionScope.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/TransactionScope.java
new file mode 100644
index 0000000000..681a2c17c0
--- /dev/null
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/annotation/TransactionScope.java
@@ -0,0 +1,79 @@
+/*
+ *  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.causeway.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";
+
+    /**
+     * Proxying <i>must</i> be enabled, because we inject {@link 
TransactionScope}d beans
+     * into beans with wider scopes.
+     *
+     * <p>Alias for {@link Scope#proxyMode}.
+     */
+    @AliasFor(annotation = Scope.class)
+    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
+
+}
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java
index ebd70f2d79..868f0252f7 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/xactn/TransactionalProcessor.java
@@ -44,6 +44,10 @@ public interface TransactionalProcessor {
     /**
      * Runs given {@code callable} with a transactional boundary, where the 
detailed transactional behavior
      * is governed by given {@link TransactionDefinition} {@code def}.
+     *
+     * @param def - transaction definition, in particular whether to use 
existing or start new transaction.  Requires only a single {@link 
org.springframework.transaction.PlatformTransactionManager} to be configured 
(unless a {@link org.springframework.transaction.support.TransactionTemplate} 
is provided which wraps a specific {@link 
org.springframework.transaction.PlatformTransactionManager}.
+     * @param callable - the work to be performed within the transaction.
+     * @return {@link Try} of calling given {@code callable}
      * @return {@link Try} of calling given {@code callable}
      */
     <T> Try<T> callTransactional(TransactionDefinition def, Callable<T> 
callable);
@@ -51,6 +55,10 @@ public interface TransactionalProcessor {
     /**
      * Runs given {@code runnable} with a transactional boundary, where the 
detailed transactional behavior
      * is governed by given {@link TransactionDefinition} {@code def}.
+     *
+     * @param def - transaction definition, in particular whether to use 
existing or start new transaction.  Requires only a single {@link 
org.springframework.transaction.PlatformTransactionManager} to be configured 
(unless a {@link org.springframework.transaction.support.TransactionTemplate} 
is provided which wraps a specific {@link 
org.springframework.transaction.PlatformTransactionManager}.
+     * @param runnable - the work to be performed within the transaction.
+     * @return {@link Try} of calling given {@code callable}
      */
     default Try<Void> runTransactional(final TransactionDefinition def, final 
ThrowingRunnable runnable) {
         return callTransactional(def, ThrowingRunnable.toCallable(runnable));
@@ -61,8 +69,9 @@ public interface TransactionalProcessor {
     /**
      * Runs given {@code callable} with a transactional boundary, where the 
detailed transactional behavior
      * is governed by given {@link Propagation} {@code propagation}.
-     * <p>
-     * More fine grained control is given via {@link 
#callTransactional(TransactionDefinition, Callable)}
+     *
+     * @param propagation - transaction propagation, ie whether to use 
existing or start new transaction.  Requires only a single {@link 
org.springframework.transaction.PlatformTransactionManager} to be configured.  
For more control, use {@link #callTransactional(TransactionDefinition, 
Callable)} and pass in a {@link 
org.springframework.transaction.support.TransactionTemplate} is provided which 
wraps a specific {@link 
org.springframework.transaction.PlatformTransactionManager}.
+     * @param callable - the work to be performed within the transaction.
      * @return {@link Try} of calling given {@code callable}
      */
     default <T> Try<T> callTransactional(final Propagation propagation, final 
Callable<T> callable) {
@@ -77,6 +86,10 @@ public interface TransactionalProcessor {
      * <p>
      * More fine grained control is given via
      * {@link #runTransactional(TransactionDefinition, ThrowingRunnable)}
+     *
+     * @param propagation - transaction propagation, ie whether to use 
existing or start new transaction.  Requires only a single {@link 
org.springframework.transaction.PlatformTransactionManager} to be configured.  
For more control, use {@link #callTransactional(TransactionDefinition, 
Callable)} and pass in a {@link 
org.springframework.transaction.support.TransactionTemplate} is provided which 
wraps a specific {@link 
org.springframework.transaction.PlatformTransactionManager}.
+     * @param runnable - the work to be performed within the transaction.
+     * @return {@link Try} of calling given {@code callable}
      */
     default Try<Void> runTransactional(final Propagation propagation, final 
ThrowingRunnable runnable) {
         return callTransactional(propagation, 
ThrowingRunnable.toCallable(runnable));
diff --git a/core/interaction/src/main/java/module-info.java 
b/core/interaction/src/main/java/module-info.java
index 21e6719613..c13a521cc3 100644
--- a/core/interaction/src/main/java/module-info.java
+++ b/core/interaction/src/main/java/module-info.java
@@ -18,7 +18,6 @@
  */
 module org.apache.causeway.core.interaction {
     exports org.apache.causeway.core.interaction;
-    exports org.apache.causeway.core.interaction.integration;
     exports org.apache.causeway.core.interaction.scope;
     exports org.apache.causeway.core.interaction.session;
 
diff --git 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java
 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java
index c915622590..13ff6e5897 100644
--- 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java
+++ 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/CausewayModuleCoreInteraction.java
@@ -21,14 +21,14 @@ package org.apache.causeway.core.interaction;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
-import 
org.apache.causeway.core.interaction.integration.InteractionAwareTransactionalBoundaryHandler;
+import 
org.apache.causeway.core.interaction.scope.TransactionScopeBeanFactoryPostProcessor;
 import 
org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor;
 
 @Configuration
 @Import({
 
     InteractionScopeBeanFactoryPostProcessor.class,
-    InteractionAwareTransactionalBoundaryHandler.class
+    TransactionScopeBeanFactoryPostProcessor.class,
 
 })
 public class CausewayModuleCoreInteraction {
diff --git 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java
 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java
deleted file mode 100644
index d5164556c8..0000000000
--- 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/integration/InteractionAwareTransactionalBoundaryHandler.java
+++ /dev/null
@@ -1,175 +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.causeway.core.interaction.integration;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Consumer;
-
-import javax.annotation.Priority;
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction;
-import org.springframework.beans.factory.annotation.Qualifier;
-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.TransactionTemplate;
-
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.functional.ThrowingRunnable;
-import org.apache.causeway.commons.internal.collections._Lists;
-import org.apache.causeway.commons.internal.debug._Probe;
-import org.apache.causeway.core.interaction.session.CausewayInteraction;
-
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
-import lombok.Value;
-import lombok.val;
-import lombok.extern.log4j.Log4j2;
-
-@Service
-@Named(InteractionAwareTransactionalBoundaryHandler.LOGICAL_TYPE_NAME)
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
-@Log4j2
-public class InteractionAwareTransactionalBoundaryHandler {
-
-    public static final String LOGICAL_TYPE_NAME = 
CausewayModuleCoreInteraction.NAMESPACE + 
".InteractionAwareTransactionalBoundaryHandler";
-
-    private final Can<PlatformTransactionManager> txManagers;
-
-    @Inject
-    public InteractionAwareTransactionalBoundaryHandler(final 
List<PlatformTransactionManager> txManagers) {
-        this.txManagers = Can.ofCollection(txManagers);
-    }
-
-    // -- OPEN
-
-    public void onOpen(final @NonNull CausewayInteraction interaction) {
-
-        if (log.isDebugEnabled()) {
-            log.debug("opening on {}", _Probe.currentThreadId());
-        }
-
-        if(txManagers.isEmpty()) {
-            return; // nothing to do
-        }
-
-        val onCloseTasks = _Lists.<CloseTask>newArrayList(txManagers.size());
-        interaction.putAttribute(OnCloseHandle.class, new 
OnCloseHandle(onCloseTasks));
-
-        
txManagers.forEach(txManager->newTransactionOrParticipateInExisting(txManager, 
onCloseTasks::add));
-
-    }
-
-    // -- CLOSE
-
-    public void onClose(final @NonNull CausewayInteraction interaction) {
-
-        if (log.isDebugEnabled()) {
-            log.debug("closing on {}", _Probe.currentThreadId());
-        }
-
-        if(txManagers.isEmpty()) {
-            return; // nothing to do
-        }
-
-        Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
-                .ifPresent(OnCloseHandle::runOnCloseTasks);
-
-    }
-
-    public void requestRollback(final @NonNull CausewayInteraction 
interaction) {
-        Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
-                .ifPresent(OnCloseHandle::requestRollback);
-    }
-
-    // -- HELPER
-
-    private void newTransactionOrParticipateInExisting(
-            final PlatformTransactionManager txManager,
-            final Consumer<CloseTask> onNewCloseTask) {
-
-        val txTemplate = new TransactionTemplate(txManager);
-        
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
-
-        // either participate in existing or create new transaction
-        val txStatus = txManager.getTransaction(txTemplate);
-        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;
-        }
-
-        // we have created a new transaction, so need to provide a CloseTask
-
-        onNewCloseTask.accept(
-            new CloseTask(
-                    txStatus,
-                    txManager.getClass().getName(), // info to be used for 
display in case of errors
-                    ()->{
-
-                        if(txStatus.isRollbackOnly()) {
-                            txManager.rollback(txStatus);
-                        } else {
-                            txManager.commit(txStatus);
-                        }
-
-                    }));
-    }
-
-    @Value
-    private static class CloseTask {
-        private final @NonNull TransactionStatus txStatus;
-        private final @NonNull String onErrorInfo;
-        private final @NonNull ThrowingRunnable runnable;
-    }
-
-    @RequiredArgsConstructor
-    private static class OnCloseHandle {
-        private final @NonNull List<CloseTask> onCloseTasks;
-        void requestRollback() {
-            onCloseTasks.forEach(onCloseTask->{
-                onCloseTask.txStatus.setRollbackOnly();
-            });
-        }
-        void runOnCloseTasks() {
-            onCloseTasks.forEach(onCloseTask->{
-
-                try {
-                    onCloseTask.getRunnable().run();
-                } catch(final Throwable ex) {
-                    // ignore
-                    log.error(
-                            "failed to close transactional boundary using 
transaction-manager {}; "
-                            + "continuing to avoid memory leakage",
-                            onCloseTask.getOnErrorInfo(),
-                            ex);
-                }
-
-            });
-        }
-    }
-
-
-}
diff --git 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
index 02c0a4d07e..1b9ea5b7b6 100644
--- 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/InteractionScopeBeanFactoryPostProcessor.java
+++ 
b/core/interaction/src/main/java/org/apache/causeway/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.causeway.applib.annotation.InteractionScope.SCOPE_NAME;
 
     @Override
     public void postProcessBeanFactory(ConfigurableListableBeanFactory 
beanFactory) throws BeansException {
diff --git 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java
 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java
new file mode 100644
index 0000000000..80ca608509
--- /dev/null
+++ 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/StackedTransactionScope.java
@@ -0,0 +1,184 @@
+
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed 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
+ *
+ *      https://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.causeway.core.interaction.scope;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Stack;
+
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.config.Scope;
+import org.springframework.lang.Nullable;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+public class StackedTransactionScope implements Scope {
+
+
+    @Override
+       public Object get(String name, ObjectFactory<?> objectFactory) {
+
+        Object key = currentKeyOnTransactionStack();
+
+        ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) 
TransactionSynchronizationManager.getResource(key);
+               if (scopedObjects == null) {
+                       scopedObjects = new ScopedObjectsHolder();
+                       
TransactionSynchronizationManager.registerSynchronization(new 
CleanupSynchronization(scopedObjects));
+                       TransactionSynchronizationManager.bindResource(key, 
scopedObjects);
+               }
+               // NOTE: Do NOT modify the following to use 
Map::computeIfAbsent. For details,
+               // see 
https://github.com/spring-projects/spring-framework/issues/25801.
+               Object scopedObject = scopedObjects.scopedInstances.get(name);
+               if (scopedObject == null) {
+                       scopedObject = objectFactory.getObject();
+                       scopedObjects.scopedInstances.put(name, scopedObject);
+               }
+               return scopedObject;
+       }
+
+    @Override
+       @Nullable
+       public Object remove(String name) {
+        Object key = currentKeyOnTransactionStack();
+        ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) 
TransactionSynchronizationManager.getResource(key);
+               if (scopedObjects != null) {
+                       scopedObjects.destructionCallbacks.remove(name);
+                       return scopedObjects.scopedInstances.remove(name);
+               }
+               else {
+                       return null;
+               }
+       }
+
+       @Override
+       public void registerDestructionCallback(String name, Runnable callback) 
{
+               ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder) 
TransactionSynchronizationManager.getResource(this);
+               if (scopedObjects != null) {
+                       scopedObjects.destructionCallbacks.put(name, callback);
+               }
+       }
+
+    private static final ThreadLocal<Stack<Object>> 
transactionStackThreadLocal = ThreadLocal.withInitial(() -> {
+        Stack<Object> stack = new Stack<>();
+        stack.push(new Object());
+        return stack;
+    });
+
+    /**
+     * Maintains a stack of keys, where the top-most is the key managed by 
{@link TransactionSynchronizationManager}
+     * holding the {@link ScopedObjectsHolder} for the current transaction.
+     *
+     * <p>
+     *     If a transaction is suspended, then the {@link 
CleanupSynchronization#suspend() suspend} callback is used
+     *     to pop a new key onto the stack, unbinding the previous key's 
resources (in other words, the
+     *     {@link org.apache.causeway.applib.annotation.TransactionScope 
transaction-scope}d beans of the suspended
+     *     transaction) from {@link TransactionSynchronizationManager}.  As 
transaction-scoped beans are then resolved,
+     *     they will be associated with the new key.
+     * </p>
+     *
+     * <p>
+     *     Conversely, when a tranaction is resumed, then the process is 
reversed; the old key is popped, and the previous
+     *     key is rebound to the {@link TransactionSynchronizationManager}, 
meaning that the previous transaction's
+     *     {@link org.apache.causeway.applib.annotation.TransactionScope 
transaction-scope}d beans are brought back.
+     * </p>
+     *
+     * @see #currentKeyOnTransactionStack()
+     * @see #pushNewKeyOntoTransactionStack()
+     * @see #popOldKeyFromTransactionStack()
+     * @see #transactionStackThreadLocal
+     */
+    private static Stack<Object> transactionStack() {
+        return transactionStackThreadLocal.get();
+    }
+    /**
+     * @see #transactionStack()
+     */
+    private Object currentKeyOnTransactionStack() {
+        return transactionStack().peek();
+    }
+    /**
+     * @see #transactionStack()
+     */
+    private static void pushNewKeyOntoTransactionStack() {
+        transactionStack().push(new Object());
+    }
+    /**
+     * @see #transactionStack()
+     */
+    private static void popOldKeyFromTransactionStack() {
+        transactionStack().pop();
+    }
+
+
+    @Override
+       @Nullable
+       public Object resolveContextualObject(String key) {
+               return null;
+       }
+
+       @Override
+       @Nullable
+       public String getConversationId() {
+               return 
TransactionSynchronizationManager.getCurrentTransactionName();
+       }
+
+
+       /**
+        * Holder for scoped objects.
+        */
+       static class ScopedObjectsHolder {
+
+               final Map<String, Object> scopedInstances = new HashMap<>();
+
+               final Map<String, Runnable> destructionCallbacks = new 
LinkedHashMap<>();
+       }
+
+
+       private class CleanupSynchronization implements 
TransactionSynchronization {
+
+               private final ScopedObjectsHolder scopedObjects;
+
+               public CleanupSynchronization(ScopedObjectsHolder 
scopedObjects) {
+                       this.scopedObjects = scopedObjects;
+               }
+
+               @Override
+               public void suspend() {
+            
TransactionSynchronizationManager.unbindResource(currentKeyOnTransactionStack());
+            pushNewKeyOntoTransactionStack();  // subsequent calls to obtain a 
@TransactionScope'd bean will be against this key
+               }
+
+               @Override
+               public void resume() {
+            popOldKeyFromTransactionStack(); // the now-completed 
transaction's @TransactionScope'd beans are no longer required, and will be 
GC'd.
+                       
TransactionSynchronizationManager.bindResource(currentKeyOnTransactionStack(), 
this.scopedObjects);
+               }
+
+               @Override
+               public void afterCompletion(int status) {
+                       
TransactionSynchronizationManager.unbindResourceIfPossible(StackedTransactionScope.this.currentKeyOnTransactionStack());
+                       for (Runnable callback : 
this.scopedObjects.destructionCallbacks.values()) {
+                               callback.run();
+                       }
+                       this.scopedObjects.destructionCallbacks.clear();
+                       this.scopedObjects.scopedInstances.clear();
+               }
+       }
+
+}
diff --git 
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java
 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
similarity index 50%
rename from 
core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java
rename to 
core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
index 8bad9c8f54..51deb40f56 100644
--- 
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionBeforeCompletionEvent.java
+++ 
b/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionScopeBeanFactoryPostProcessor.java
@@ -16,17 +16,27 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.transaction.events;
+package org.apache.causeway.core.interaction.scope;
 
-import lombok.Value;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import 
org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.stereotype.Component;
 
-@Value
-public class TransactionBeforeCompletionEvent {
+import lombok.val;
 
-    private final static TransactionBeforeCompletionEvent INSTANCE = new 
TransactionBeforeCompletionEvent();
+/**
+ * @since 2.0
+ */
+@Component
+public class TransactionScopeBeanFactoryPostProcessor implements 
BeanFactoryPostProcessor {
+
+    public static final String SCOPE_NAME = 
org.apache.causeway.applib.annotation.TransactionScope.SCOPE_NAME;
 
-    public static TransactionBeforeCompletionEvent instance() {
-        return INSTANCE;
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory 
beanFactory) throws BeansException {
+        val transactionScope = new StackedTransactionScope();
+        beanFactory.registerScope(SCOPE_NAME, transactionScope);
     }
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java
index 4c6d4e90ec..3186cdde8f 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/objectmanager/ObjectCreator.java
@@ -78,7 +78,9 @@ interface ObjectCreator {
             
domainObject.getSpecification().streamAssociations(MixedIn.EXCLUDED)
             .forEach(field->field.toDefault(domainObject));
 
-            getPersistenceLifecyclePublisher().onPostCreate(domainObject);
+            if (domainObject.getSpecification().isEntity()) {
+                getPersistenceLifecyclePublisher().onPostCreate(domainObject);
+            }
 
             return domainObject;
 
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
index 4707fac77a..488f6f3828 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/objectlifecycle/ObjectLifecyclePublisher.java
@@ -32,6 +32,10 @@ import 
org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
  * Responsible for collecting and then passing along changes (to the 
EntityChangeTracker, in persistence commons) so
  * that they can be published; and is responsible for calling the various 
persistence call-back facets.
  *
+ * <p>
+ *     NOTE: this should really have been called ObjectLifecyclePublisher.
+ * </p>
+ *
  * @since 2.0 {index}
  */
 public interface ObjectLifecyclePublisher {
diff --git 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java
similarity index 54%
rename from 
core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java
index a61d5a5567..094fc4d5d5 100644
--- 
a/core/interaction/src/main/java/org/apache/causeway/core/interaction/scope/TransactionBoundaryAware.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/IntrospectionState.java
@@ -16,26 +16,32 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.interaction.scope;
-
-import org.apache.causeway.applib.services.iactn.Interaction;
-
-public interface TransactionBoundaryAware {
-
-    default void beforeEnteringTransactionalBoundary(Interaction interaction) {
-
-    }
-
-    default void afterEnteringTransactionalBoundary(Interaction interaction, 
boolean isSynchronizationActive) {
-
-    }
-
-    default void beforeLeavingTransactionalBoundary(Interaction interaction, 
boolean isSynchronizationActive) {
-
-    }
-
-    default void afterLeavingTransactionalBoundary(Interaction interaction) {
-
-    }
+package org.apache.causeway.core.metamodel.specloader.specimpl;
+
+public enum IntrospectionState implements Comparable<IntrospectionState> {
+    /**
+     * At this stage, {@link LogicalTypeFacet} only.
+     */
+    NOT_INTROSPECTED,
+
+    /**
+     * Interim stage, to avoid infinite loops while on way to being {@link 
#TYPE_INTROSPECTED}
+     */
+    TYPE_BEING_INTROSPECTED,
+
+    /**
+     * Type has been introspected (but not its members).
+     */
+    TYPE_INTROSPECTED,
+
+    /**
+     * Interim stage, to avoid infinite loops while on way to being {@link 
#FULLY_INTROSPECTED}
+     */
+    MEMBERS_BEING_INTROSPECTED,
+
+    /**
+     * Fully introspected... class and also its members.
+     */
+    FULLY_INTROSPECTED
 
 }
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
index 0db123e2fc..0218c9bd96 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/CausewayModuleCoreRuntime.java
@@ -24,7 +24,6 @@ import org.springframework.context.annotation.Import;
 import org.apache.causeway.core.interaction.CausewayModuleCoreInteraction;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
 import org.apache.causeway.core.runtime.events.MetamodelEventService;
-import org.apache.causeway.core.runtime.events.TransactionEventEmitter;
 import org.apache.causeway.core.transaction.CausewayModuleCoreTransaction;
 import 
org.apache.causeway.valuetypes.jodatime.integration.CausewayModuleValJodatimeIntegration;
 
@@ -40,7 +39,6 @@ import 
org.apache.causeway.valuetypes.jodatime.integration.CausewayModuleValJoda
 
         // @Service's
         MetamodelEventService.class,
-        TransactionEventEmitter.class,
 
         // @Configuration's
 
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java
 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java
deleted file mode 100644
index b49a2bba89..0000000000
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/TransactionEventEmitter.java
+++ /dev/null
@@ -1,67 +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.causeway.core.runtime.events;
-
-import javax.inject.Inject;
-
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.support.TransactionSynchronization;
-import 
org.springframework.transaction.support.TransactionSynchronizationManager;
-
-import org.apache.causeway.applib.services.eventbus.EventBusService;
-import org.apache.causeway.applib.services.iactn.Interaction;
-import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
-import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
-import 
org.apache.causeway.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(
-            final Interaction interaction,
-            final boolean isSynchronizationActive) {
-        if(isSynchronizationActive) {
-            TransactionSynchronizationManager.registerSynchronization(this);
-        }
-    }
-
-}
diff --git 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java
index 697e7ffb01..f1f415e7e1 100644
--- 
a/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java
+++ 
b/core/runtime/src/main/java/org/apache/causeway/core/runtime/events/_Xray.java
@@ -29,7 +29,7 @@ import org.apache.causeway.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/module-info.java 
b/core/runtimeservices/src/main/java/module-info.java
index 69986102e3..33badc9675 100644
--- a/core/runtimeservices/src/main/java/module-info.java
+++ b/core/runtimeservices/src/main/java/module-info.java
@@ -79,7 +79,8 @@ module org.apache.causeway.core.runtimeservices {
     requires spring.core;
     requires spring.tx;
     requires org.apache.causeway.core.codegen.bytebuddy;
+    requires spring.aop;
 
     opens org.apache.causeway.core.runtimeservices.wrapper;
     opens org.apache.causeway.core.runtimeservices.wrapper.proxy; //to 
org.apache.causeway.core.codegen.bytebuddy
-}
\ No newline at end of file
+}
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
index cd77ffc77c..2f2e256bfd 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/EntityPropertyChangePublisherDefault.java
@@ -26,13 +26,12 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
-import org.apache.causeway.applib.services.iactnlayer.InteractionService;
-
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.annotation.TransactionScope;
 import org.apache.causeway.applib.services.clock.ClockService;
 import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
 import org.apache.causeway.applib.services.publishing.spi.EntityPropertyChange;
@@ -63,7 +62,6 @@ public class EntityPropertyChangePublisherDefault implements 
EntityPropertyChang
     private final UserService userService;
     private final ClockService clockService;
     private final TransactionService transactionService;
-    private final InteractionService interactionService;
     private final InteractionLayerTracker iaTracker;
     private final Provider<HasEnlistedEntityPropertyChanges> 
hasEnlistedEntityPropertyChangesProvider;
 
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
index b2115fc1ed..a13b5c02e3 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/publish/ObjectLifecyclePublisherDefault.java
@@ -31,6 +31,7 @@ import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.annotation.TransactionScope;
 import org.apache.causeway.applib.services.iactnlayer.InteractionService;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.Either;
@@ -47,6 +48,7 @@ import lombok.RequiredArgsConstructor;
  * @since 2.0 {@index}
  */
 @Service
+@TransactionScope
 @Named(CausewayModuleCoreRuntimeServices.NAMESPACE + 
".ObjectLifecyclePublisherDefault")
 @Priority(PriorityPrecedence.EARLY)
 @Qualifier("Default")
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
index b59d004901..7ebe1c7f8e 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/session/InteractionServiceDefault.java
@@ -19,7 +19,6 @@
 package org.apache.causeway.core.runtimeservices.session;
 
 import java.io.File;
-import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Stack;
@@ -31,13 +30,13 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import 
org.apache.causeway.core.runtimeservices.transaction.TransactionServiceSpring;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.config.ConfigurableBeanFactory;
-import org.springframework.context.annotation.Lazy;
 import org.springframework.context.event.ContextRefreshedEvent;
 import org.springframework.context.event.EventListener;
 import org.springframework.stereotype.Service;
-import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.clock.ClockService;
@@ -60,10 +59,8 @@ import 
org.apache.causeway.commons.internal.concurrent._ConcurrentTaskList;
 import org.apache.causeway.commons.internal.debug._Probe;
 import org.apache.causeway.commons.internal.debug.xray.XrayUi;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import 
org.apache.causeway.core.interaction.integration.InteractionAwareTransactionalBoundaryHandler;
 import 
org.apache.causeway.core.interaction.scope.InteractionScopeBeanFactoryPostProcessor;
 import 
org.apache.causeway.core.interaction.scope.InteractionScopeLifecycleHandler;
-import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware;
 import org.apache.causeway.core.interaction.session.CausewayInteraction;
 import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
@@ -91,30 +88,32 @@ implements
     InteractionService,
     InteractionLayerTracker {
 
+    // TODO: reading the javadoc for TransactionSynchronizationManager and 
looking at the implementations
+    //  of TransactionSynchronization (in particular 
SpringSessionSynchronization), I suspect that this
+    //  ThreadLocal would be considered bad practice and instead should be 
managed using the TransactionSynchronization mechanism.
     final ThreadLocal<Stack<InteractionLayer>> interactionLayerStack = 
ThreadLocal.withInitial(Stack::new);
 
     final MetamodelEventService runtimeEventService;
     final SpecificationLoader specificationLoader;
     final ServiceInjector serviceInjector;
 
-    final InteractionAwareTransactionalBoundaryHandler txBoundaryHandler;
     final ClockService clockService;
     final Provider<CommandPublisher> commandPublisherProvider;
     final Provider<TransactionService> transactionServiceProvider;
     final ConfigurableBeanFactory beanFactory;
 
     final InteractionScopeLifecycleHandler interactionScopeLifecycleHandler;
+    final TransactionServiceSpring transactionServiceSpring;
+
     final InteractionIdGenerator interactionIdGenerator;
 
-    // to allow implementations to have dependencies back on this service.
-    @Inject @Lazy List<TransactionBoundaryAware> transactionBoundaryAwareBeans;
 
     @Inject
     public InteractionServiceDefault(
             final MetamodelEventService runtimeEventService,
             final SpecificationLoader specificationLoader,
             final ServiceInjector serviceInjector,
-            final InteractionAwareTransactionalBoundaryHandler 
txBoundaryHandler,
+            final TransactionServiceSpring transactionServiceSpring,
             final ClockService clockService,
             final Provider<CommandPublisher> commandPublisherProvider,
             final Provider<TransactionService> transactionServiceProvider,
@@ -123,7 +122,7 @@ implements
         this.runtimeEventService = runtimeEventService;
         this.specificationLoader = specificationLoader;
         this.serviceInjector = serviceInjector;
-        this.txBoundaryHandler = txBoundaryHandler;
+        this.transactionServiceSpring = transactionServiceSpring;
         this.clockService = clockService;
         this.commandPublisherProvider = commandPublisherProvider;
         this.transactionServiceProvider = transactionServiceProvider;
@@ -205,7 +204,8 @@ implements
         interactionLayerStack.get().push(interactionLayer);
 
         if(isAtTopLevel()) {
-               postInteractionOpened(causewayInteraction);
+            transactionServiceSpring.onOpen(causewayInteraction);
+            interactionScopeLifecycleHandler.onTopLevelInteractionOpened();
         }
 
         if(log.isDebugEnabled()) {
@@ -355,21 +355,13 @@ implements
             return;
         }
         val interaction = 
_Casts.<CausewayInteraction>uncheckedCast(stack.get(0).getInteraction());
-        txBoundaryHandler.requestRollback(interaction);
+        transactionServiceSpring.requestRollback(interaction);
     }
 
     private boolean isAtTopLevel() {
        return interactionLayerStack.get().size()==1;
     }
 
-    private void postInteractionOpened(final CausewayInteraction interaction) {
-        
transactionBoundaryAwareBeans.forEach(bean->bean.beforeEnteringTransactionalBoundary(interaction));
-        txBoundaryHandler.onOpen(interaction);
-        val isSynchronizationActive = 
TransactionSynchronizationManager.isSynchronizationActive();
-        
transactionBoundaryAwareBeans.forEach(bean->bean.afterEnteringTransactionalBoundary(interaction,
 isSynchronizationActive));
-        interactionScopeLifecycleHandler.onTopLevelInteractionOpened();
-    }
-
     @SneakyThrows
     private void preInteractionClosed(final CausewayInteraction interaction) {
 
@@ -387,10 +379,8 @@ implements
             flushException = e;
         }
 
-        val isSynchronizationActive = 
TransactionSynchronizationManager.isSynchronizationActive();
-        
transactionBoundaryAwareBeans.forEach(bean->bean.beforeLeavingTransactionalBoundary(interaction,
 isSynchronizationActive));
-        txBoundaryHandler.onClose(interaction);
-        
transactionBoundaryAwareBeans.forEach(bean->bean.afterLeavingTransactionalBoundary(interaction));
+        transactionServiceSpring.onClose(interaction);
+
         interactionScopeLifecycleHandler.onTopLevelInteractionPreDestroy(); // 
cleanup the InteractionScope (Spring scope)
         interactionScopeLifecycleHandler.onTopLevelInteractionClosed(); // 
cleanup the InteractionScope (Spring scope)
         interaction.close(); // do this last
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
index ca4552dfbe..af7b7149ac 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
@@ -26,32 +26,42 @@ import java.util.concurrent.atomic.LongAdder;
 import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 
+import org.springframework.aop.support.AopUtils;
 import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.event.EventListener;
+import 
org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 import org.springframework.dao.DataAccessException;
 import org.springframework.dao.support.PersistenceExceptionTranslator;
 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.DefaultTransactionStatus;
+import org.springframework.transaction.support.TransactionSynchronization;
 import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.transaction.support.TransactionTemplate;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
 import org.apache.causeway.applib.services.xactn.TransactionId;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.applib.services.xactn.TransactionState;
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.functional.ThrowingRunnable;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.internal.base._NullSafe;
+import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.commons.internal.debug._Probe;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware;
+import org.apache.causeway.core.interaction.session.CausewayInteraction;
+import org.apache.causeway.core.runtime.events._Xray;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
+import org.apache.causeway.core.transaction.events.TransactionCompletionStatus;
 
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.Value;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
 
@@ -70,27 +80,35 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class TransactionServiceSpring
 implements
-    TransactionService,
-    TransactionBoundaryAware {
+    TransactionService {
 
     private final Can<PlatformTransactionManager> platformTransactionManagers;
-    private final InteractionLayerTracker interactionLayerTracker;
+    private final Provider<InteractionLayerTracker> 
interactionLayerTrackerProvider;
     private final Can<PersistenceExceptionTranslator> 
persistenceExceptionTranslators;
-
+    private final ConfigurableListableBeanFactory 
configurableListableBeanFactory;
+//    private final Can<TransactionBoundaryAware> 
transactionBoundaryAwareBeans;
 
     @Inject
     public TransactionServiceSpring(
             final List<PlatformTransactionManager> platformTransactionManagers,
             final List<PersistenceExceptionTranslator> 
persistenceExceptionTranslators,
-            final InteractionLayerTracker interactionLayerTracker) {
+            final Provider<InteractionLayerTracker> 
interactionLayerTrackerProvider,
+            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.interactionLayerTrackerProvider = interactionLayerTrackerProvider;
+
+//        this.transactionBoundaryAwareBeans = 
Can.ofCollection(transactionBoundaryAwareBeans);
+//        log.info("TransactionBoundaryAwareBeans: {}", 
transactionBoundaryAwareBeans);
     }
 
     // -- SPRING INTEGRATION
@@ -98,23 +116,33 @@ implements
     @Override
     public <T> Try<T> callTransactional(final TransactionDefinition def, final 
Callable<T> callable) {
 
-        val txManager = transactionManagerForElseFail(def); // always throws 
if configuration is wrong
+        val platformTransactionManager = transactionManagerForElseFail(def); 
// always throws if configuration is wrong
 
         Try<T> result = null;
 
         try {
 
-            val tx = txManager.getTransaction(def);
+            TransactionStatus txStatus = 
platformTransactionManager.getTransaction(def);
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> 
tba.afterEnteringTransactionalBoundary(platformTransactionManager));
+//            }
+            registerTransactionSynchronizations(txStatus);
+
 
             result = Try.call(callable)
-                    .mapFailure(ex->translateExceptionIfPossible(ex, 
txManager));
+                    .mapFailure(ex->translateExceptionIfPossible(ex, 
platformTransactionManager));
 
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> 
tba.beforeLeavingTransactionalBoundary(platformTransactionManager));
+//            }
             if(result.isFailure()) {
-                txManager.rollback(tx);
+                platformTransactionManager.rollback(txStatus);
             } else {
-                txManager.commit(tx);
+                platformTransactionManager.commit(txStatus);
             }
-
+//            if(tx.isNewTransaction()) {
+//                transactionBoundaryAwareBeans.forEach(tba -> 
tba.afterLeavingTransactionalBoundary(platformTransactionManager));
+//            }
         } catch (Exception ex) {
 
             return result!=null
@@ -125,13 +153,30 @@ implements
                     ? result
 
                     // return the failure we just catched
-                    : Try.failure(translateExceptionIfPossible(ex, txManager));
+                    : Try.failure(translateExceptionIfPossible(ex, 
platformTransactionManager));
 
         }
 
         return result;
     }
 
+
+    private void registerTransactionSynchronizations(final TransactionStatus 
txStatus) {
+        if (TransactionSynchronizationManager.isSynchronizationActive()) {
+            if (txStatus instanceof DefaultTransactionStatus) {
+                
configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
+                        .values()
+                        .stream().filter(AopUtils::isAopProxy)  // only the 
proxies
+                        
.forEach(TransactionSynchronizationManager::registerSynchronization);
+            } else {
+                
configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
+                        .values()
+                        .stream().filter(AopUtils::isAopProxy)  // only the 
proxies
+                        
.forEach(TransactionSynchronizationManager::registerSynchronization);
+            }
+        }
+    }
+
 //    @Override
 //    public void nextTransaction() {
 //
@@ -204,7 +249,7 @@ implements
 
     @Override
     public Optional<TransactionId> currentTransactionId() {
-        return interactionLayerTracker.getInteractionId()
+        return interactionLayerTrackerProvider.get().getInteractionId()
                 .map(uuid->{
                     //XXX get current transaction's persistence context (once 
we support multiple contexts)
                     val persistenceContext = "";
@@ -234,25 +279,11 @@ implements
 
     // -- TRANSACTION SEQUENCE TRACKING
 
+    // TODO: this ThreadLocal (as with all thread-locals) should perhaps 
somehow be managed using
+    //  TransactionSynchronizationManager; see its javadoc for more details 
and look at implementations of
+    //  TransactionSynchronization
     private ThreadLocal<LongAdder> txCounter = 
ThreadLocal.withInitial(LongAdder::new);
 
-    /** INTERACTION BEGIN BOUNDARY */
-    @Override
-    public void beforeEnteringTransactionalBoundary(final Interaction 
interaction) {
-        txCounter.get().reset();
-    }
-
-    /** TRANSACTION END BOUNDARY */
-    @EventListener(TransactionAfterCompletionEvent.class)
-    public void onTransactionEnded(final TransactionAfterCompletionEvent 
event) {
-        txCounter.get().increment();
-    }
-
-    /** INTERACTION END BOUNDARY */
-    @Override
-    public void afterLeavingTransactionalBoundary(final Interaction 
interaction) {
-        txCounter.remove(); //XXX not tested yet: can we be certain that no 
txCounter.get() is called afterwards?
-    }
 
     // -- HELPER
 
@@ -266,8 +297,9 @@ implements
         return platformTransactionManagers.getSingleton()
                 .orElseThrow(()->
                     platformTransactionManagers.getCardinality().isMultiple()
-                        ? _Exceptions.illegalState("Multiple 
PlatformTransactionManagers are configured, "
-                                + "make sure a PlatformTransactionManager is 
provided via the TransactionTemplate argument.")
+                        ? _Exceptions.illegalState(
+                                "Multiple PlatformTransactionManagers are 
configured, cannot determine which one to use. "
+                                + "Instead make sure a 
PlatformTransactionManager is provided explicitly by passing in a 
TransactionTemplate (implementation of TransactionDefinition).")
                         : _Exceptions.illegalState("Needs a 
PlatformTransactionManager."));
     }
 
@@ -323,4 +355,116 @@ implements
         return ex;
     }
 
+
+    public void onOpen(final @NonNull CausewayInteraction interaction) {
+
+        txCounter.get().reset();
+
+        if (log.isDebugEnabled()) {
+            log.debug("opening on {}", _Probe.currentThreadId());
+        }
+
+
+        if (!platformTransactionManagers.isEmpty()) {
+            val onCloseTasks = 
_Lists.<CloseTask>newArrayList(platformTransactionManagers.size());
+
+            interaction.putAttribute(OnCloseHandle.class, new 
OnCloseHandle(onCloseTasks));
+
+            platformTransactionManagers.forEach(txManager -> {
+
+                val txDefn = new TransactionTemplate(txManager); // specify 
the txManager in question
+                
txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
+
+                // either participate in existing or create new transaction
+                TransactionStatus txStatus = txManager.getTransaction(txDefn);
+
+                if(!txStatus.isNewTransaction()) {
+                    // we are participating in an exiting transaction (or 
testing), nothing to do
+                    return;
+                }
+                registerTransactionSynchronizations(txStatus);
+
+
+                // we have created a new transaction, so need to provide a 
CloseTask
+                onCloseTasks.add(
+                        new CloseTask(
+                            txStatus,
+                            txManager.getClass().getName(), // info to be used 
for display in case of errors
+                            () -> {
+//                                transactionBoundaryAwareBeans.forEach(tbab 
-> tbab.beforeLeavingTransactionalBoundary(txManager));
+                                
_Xray.txBeforeCompletion(interactionLayerTrackerProvider.get(), "tx: 
beforeCompletion");
+                                final TransactionCompletionStatus event;
+                                if (txStatus.isRollbackOnly()) {
+                                    txManager.rollback(txStatus);
+                                    event = 
TransactionCompletionStatus.ROLLED_BACK;
+                                } else {
+                                    txManager.commit(txStatus);
+                                    event = 
TransactionCompletionStatus.COMMITTED;
+                                }
+                                
_Xray.txAfterCompletion(interactionLayerTrackerProvider.get(), 
String.format("tx: afterCompletion (%s)", event.name()));
+
+//                                transactionBoundaryAwareBeans.forEach(tbab 
-> tbab.afterLeavingTransactionalBoundary(txManager));
+                                txCounter.get().increment();
+                            }
+                        )
+                );
+
+            });
+        }
+
+    }
+
+
+    public void requestRollback(final @NonNull CausewayInteraction 
interaction) {
+        Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
+                .ifPresent(OnCloseHandle::requestRollback);
+    }
+
+    public void onClose(final @NonNull CausewayInteraction interaction) {
+
+        if (log.isDebugEnabled()) {
+            log.debug("closing on {}", _Probe.currentThreadId());
+        }
+
+        if (!platformTransactionManagers.isEmpty()) {
+            Optional.ofNullable(interaction.getAttribute(OnCloseHandle.class))
+                    .ifPresent(OnCloseHandle::runOnCloseTasks);
+        }
+
+        txCounter.remove(); //XXX not tested yet: can we be certain that no 
txCounter.get() is called afterwards?
+    }
+
+    @Value
+    private static class CloseTask {
+        @NonNull TransactionStatus txStatus;
+        @NonNull String onErrorInfo;
+        @NonNull ThrowingRunnable runnable;
+    }
+
+    @RequiredArgsConstructor
+    private static class OnCloseHandle {
+        private final @NonNull List<CloseTask> onCloseTasks;
+        void requestRollback() {
+            onCloseTasks.forEach(onCloseTask->{
+                onCloseTask.txStatus.setRollbackOnly();
+            });
+        }
+        void runOnCloseTasks() {
+            onCloseTasks.forEach(onCloseTask->{
+
+                try {
+                    onCloseTask.getRunnable().run();
+                } catch(final Throwable ex) {
+                    // ignore
+                    log.error(
+                            "failed to close transactional boundary using 
transaction-manager {}; "
+                                    + "continuing to avoid memory leakage",
+                            onCloseTask.getOnErrorInfo(),
+                            ex);
+                }
+
+            });
+        }
+    }
+
 }
diff --git a/core/transaction/src/main/java/module-info.java 
b/core/transaction/src/main/java/module-info.java
index f3fc966546..d64c8c9e33 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.causeway.core.transaction {
     requires spring.context;
     requires spring.core;
     requires spring.tx;
-}
\ No newline at end of file
+    requires java.transaction;
+}
diff --git 
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java
 
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java
similarity index 60%
rename from 
core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java
rename to 
core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java
index 3e8fbfeb4d..9117afaece 100644
--- 
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionAfterCompletionEvent.java
+++ 
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/events/TransactionCompletionStatus.java
@@ -18,13 +18,13 @@
  */
 package org.apache.causeway.core.transaction.events;
 
-import org.springframework.transaction.support.TransactionSynchronization;
+import javax.transaction.Status;
 
 /**
  * @since 2.0 {@index}
- * @see TransactionSynchronization
+ * @see Status
  */
-public enum TransactionAfterCompletionEvent {
+public enum TransactionCompletionStatus {
 
     /** Completion status in case of proper commit. */
     COMMITTED,
@@ -36,8 +36,29 @@ public enum TransactionAfterCompletionEvent {
     UNKNOWN,
     ;
 
-    public static TransactionAfterCompletionEvent forStatus(int status) {
-        return TransactionAfterCompletionEvent.values()[status];
+    /**
+     * @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() {
diff --git 
a/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java
 
b/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java
index 77cab60cb2..a9c0166d87 100644
--- 
a/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java
+++ 
b/extensions/core/executionoutbox/restclient/src/test/java/org/apache/causeway/extensions/executionoutbox/restclient/integtests/OutboxRestClient_E2eTest.java
@@ -49,9 +49,9 @@ public class OutboxRestClient_E2eTest {
                         .oauthClientSecret("xxx")
                         .build(),
                 OutboxClientConfig.builder()
-                        
.pendingUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/pending/invoke")
-                        
.deleteUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/delete/invoke")
-                        
.deleteManyUri("services/isis.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke")
+                        
.pendingUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/pending/invoke")
+                        
.deleteUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/delete/invoke")
+                        
.deleteManyUri("services/causeway.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke")
                         .build()
                 );
     }
diff --git 
a/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java
 
b/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java
index a2aace75a7..5881da3d40 100644
--- 
a/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java
+++ 
b/extensions/security/sessionlog/applib/src/main/java/org/apache/causeway/extensions/sessionlog/applib/spiimpl/SessionSubscriberForSessionLog.java
@@ -69,18 +69,17 @@ public class SessionSubscriberForSessionLog implements 
SessionSubscriber {
     public void log(final Type type, final String username, final Date date, 
final CausedBy causedBy, final UUID sessionGuid, final String httpSessionId) {
         interactionService.runAnonymous(() -> {
             transactionService.runTransactional(Propagation.REQUIRES_NEW, () 
-> {
+                val sessionLogEntryIfAny = 
sessionLogEntryRepository.findBySessionGuid(sessionGuid);
                 if (type == Type.LOGIN) {
-                    sessionLogEntryRepository.create(username, sessionGuid, 
httpSessionId, causedBy, Timestamp.from(date.toInstant()));
+                    if (sessionLogEntryIfAny.isEmpty()) {
+                        sessionLogEntryRepository.create(username, 
sessionGuid, httpSessionId, causedBy, Timestamp.from(date.toInstant()));
+                    }
                 } else {
-
-                    val sessionLogEntryIfAny = 
sessionLogEntryRepository.findBySessionGuid(sessionGuid);
-                    sessionLogEntryIfAny
-                            .ifPresent(entry -> {
-                                        
entry.setLogoutTimestamp(Timestamp.from(date.toInstant()));
-                                        entry.setCausedBy(causedBy);
-                                        transactionService.flushTransaction();
-                                    }
-                            );
+                    sessionLogEntryIfAny.ifPresent(entry -> {
+                        
entry.setLogoutTimestamp(Timestamp.from(date.toInstant()));
+                        entry.setCausedBy(causedBy);
+                        transactionService.flushTransaction();
+                    });
                 }
             })
             .ifFailureFail(); // throw if rolled back
diff --git a/persistence/commons/src/main/java/module-info.java 
b/persistence/commons/src/main/java/module-info.java
index ddc1bccd1e..58a535970c 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.causeway.persistence.commons {
     requires spring.beans;
     requires spring.context;
     requires spring.core;
-}
\ No newline at end of file
+    requires spring.tx;
+}
diff --git 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java
 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java
index 58d7f24b6e..ac62cade36 100644
--- 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java
+++ 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/CausewayModulePersistenceCommons.java
@@ -32,7 +32,6 @@ import 
org.apache.causeway.persistence.commons.integration.changetracking.PreAnd
 
         // @Service's
         EntityChangeTrackerDefault.class,
-        EntityChangeTrackerDefault.TransactionSubscriber.class,
         PreAndPostValueEvaluatorServiceDefault.class,
 })
 public class CausewayModulePersistenceCommons {
diff --git 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
index 6a404701eb..dbf8911f1b 100644
--- 
a/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
+++ 
b/persistence/commons/src/main/java/org/apache/causeway/persistence/commons/integration/changetracking/EntityChangeTrackerDefault.java
@@ -27,26 +27,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.LongAdder;
 import java.util.function.Function;
 
-import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
 import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.event.EventListener;
-import org.springframework.core.annotation.Order;
+import org.springframework.core.Ordered;
 import org.springframework.lang.Nullable;
-import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.EntityChangeKind;
-import org.apache.causeway.applib.annotation.InteractionScope;
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.annotation.TransactionScope;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.iactn.InteractionProvider;
-import org.apache.causeway.applib.services.iactnlayer.InteractionService;
 import org.apache.causeway.applib.services.metrics.MetricsService;
 import org.apache.causeway.applib.services.publishing.spi.EntityChanges;
 import org.apache.causeway.applib.services.publishing.spi.EntityPropertyChange;
@@ -68,7 +67,6 @@ import 
org.apache.causeway.core.transaction.changetracking.EntityChangeTracker;
 import 
org.apache.causeway.core.transaction.changetracking.EntityChangesPublisher;
 import 
org.apache.causeway.core.transaction.changetracking.EntityPropertyChangePublisher;
 import 
org.apache.causeway.core.transaction.changetracking.HasEnlistedEntityChanges;
-import 
org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent;
 
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -78,24 +76,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 {@link TransactionScope transaction-scope}d and implements 
Spring's {@link TransactionSynchronization}
+ * interface, meaning that Spring will call the {@link #beforeCompletion()} 
callback.  This service also implements
+ * {@link org.springframework.core.Ordered} to ensure it isn't called last by 
{@link TransactionSynchronizationManager}.
  * </p>
  *
  * @since 2.0 {@index}
  */
 @Service
+@TransactionScope
 @Named("causeway.persistence.commons.EntityChangeTrackerDefault")
-@Priority(PriorityPrecedence.EARLY)
 @Qualifier("default")
-@InteractionScope   // see note above regarding this
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
 @Log4j2
 public class EntityChangeTrackerDefault
@@ -103,8 +99,15 @@ implements
     MetricsService,
     EntityChangeTracker,
     HasEnlistedEntityPropertyChanges,
-    HasEnlistedEntityChanges {
+    HasEnlistedEntityChanges,
+    TransactionSynchronization,
+    Ordered {
 
+    @Programmatic
+    @Override
+    public int getOrder() {
+        return PriorityPrecedence.EARLY;
+    }
 
     private final EntityPropertyChangePublisher entityPropertyChangePublisher;
     private final EntityChangesPublisher entityChangesPublisher;
@@ -193,83 +196,26 @@ implements
         return false;
     }
 
-    /**
-     * Subscribes to transactions and forwards onto the current interaction's 
EntityChangeTracker, if available.
-     *
-     * <p>
-     *     Note that this service has singleton-scope, unlike {@link 
EntityChangeTrackerDefault} which has
-     *     {@link InteractionScope interaction scope}. The problem with using 
{@link EntityChangeTrackerDefault} as
-     *     the direct subscriber is that if there's no {@link Interaction}, 
then Spring will fail to activate an instance resulting in an
-     *     {@link 
org.springframework.beans.factory.support.ScopeNotActiveException}.  Now, 
admittedly that exception
-     *     gets swallowed in the call stack somewhere, but it's still not 
pretty.
-     * </p>
-     *
-     * <p>
-     *     This design, instead, at least lets us check if there's an 
interaction in scope, and effectively ignore
-     *     the call if not.
-     * </p>
-     */
-    @Component
-    
@Named("causeway.persistence.commons.EntityChangeTrackerDefault.TransactionSubscriber")
-    @Priority(PriorityPrecedence.EARLY)
-    @Qualifier("default")
-    @RequiredArgsConstructor(onConstructor_ = {@Inject})
-    public static class TransactionSubscriber {
-
-        private final InteractionService interactionService;
-        private final Provider<EntityChangeTrackerDefault> 
entityChangeTrackerProvider;
-
-        /**
-         * TRANSACTION END BOUNDARY
-         * @apiNote intended to be called during before transaction completion 
by the framework internally
-         */
-        @EventListener(value = TransactionBeforeCompletionEvent.class)
-        @Order(PriorityPrecedence.LATE)
-        public void onTransactionCompleting(final 
TransactionBeforeCompletionEvent event) {
-
-            if(!interactionService.isInInteraction()) {
-                // discard request is there is no interaction in scope.
-                // this shouldn't ever really occur, but some low-level (could 
be improved?) integration tests do
-                // hit this case.
-                return;
-            }
-            entityChangeTracker().onTransactionCompleting(event);
-        }
-
-        private EntityChangeTrackerDefault entityChangeTracker() {
-            return entityChangeTrackerProvider.get();
-        }
-    }
 
-    /**
-     * As called by {@link TransactionSubscriber}, so long as there is an 
{@link Interaction} in
-     * {@link InteractionScope scope}.
-     */
-    void onTransactionCompleting(final TransactionBeforeCompletionEvent event) 
{
+    @Override
+    public void beforeCompletion() {
         try {
-            doPublish();
-        } finally {
-            postPublishing();
-        }
-    }
+            _Xray.publish(this, interactionProviderProvider);
 
-    private void doPublish() {
-        _Xray.publish(this, interactionProviderProvider);
+            log.debug("about to publish entity changes");
+            entityPropertyChangePublisher.publishChangedProperties();
+            entityChangesPublisher.publishChangingEntities(this);
 
-        log.debug("about to publish entity changes");
-        entityPropertyChangePublisher.publishChangedProperties();
-        entityChangesPublisher.publishChangingEntities(this);
-    }
-
-    private void postPublishing() {
-        log.debug("purging entity change records");
+        } finally {
+            log.debug("purging entity change records");
 
-        enlistedPropertyChangeRecordsById.clear();
-        entityPropertyChangeRecordsForPublishing.clear();
+            enlistedPropertyChangeRecordsById.clear();
+            entityPropertyChangeRecordsForPublishing.clear();
 
-        changeKindByEnlistedAdapter.clear();
-        entityChangeEventCount.reset();
-        numberEntitiesLoaded.reset();
+            changeKindByEnlistedAdapter.clear();
+            entityChangeEventCount.reset();
+            numberEntitiesLoaded.reset();
+        }
     }
 
     private void enableCommandPublishing() {
diff --git 
a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java
 
b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java
index 598f65fb07..faa4da52c1 100644
--- 
a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java
+++ 
b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/CausewayModulePersistenceJdoDatanucleus.java
@@ -27,6 +27,7 @@ import javax.sql.DataSource;
 
 import org.datanucleus.api.jdo.JDOPersistenceManagerFactory;
 import org.datanucleus.metadata.PersistenceUnitMetaData;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
diff --git 
a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
 
b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
index b4285297a5..97f53f6eee 100644
--- 
a/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
+++ 
b/persistence/jdo/datanucleus/src/main/java/org/apache/causeway/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
@@ -32,6 +32,7 @@ import javax.jdo.PersistenceManager;
 import org.datanucleus.api.jdo.JDOQuery;
 import org.datanucleus.enhancement.Persistable;
 import org.datanucleus.store.rdbms.RDBMSPropertyNames;
+
 import org.springframework.lang.Nullable;
 
 import org.apache.causeway.applib.query.AllInstancesQuery;
diff --git 
a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java
 
b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java
index 9621f025b6..fb80bc4358 100644
--- 
a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java
+++ 
b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingInteractionService.java
@@ -43,8 +43,6 @@ import 
org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.commons.internal.base._Refs;
 import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
 import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
-import 
org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent;
 import org.apache.causeway.testdomain.conf.Configuration_usingJpa;
 import org.apache.causeway.testdomain.jpa.JpaTestDomainPersona;
 import org.apache.causeway.testdomain.jpa.entities.JpaBook;
diff --git 
a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java
 
b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java
index ff09d6858f..6d295dd39e 100644
--- 
a/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java
+++ 
b/regressiontests/stable-persistence-jpa/src/test/java/org/apache/causeway/testdomain/transactions/jpa/JpaTransactionRollbackTest_usingTransactionService.java
@@ -42,8 +42,6 @@ import 
org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.commons.internal.base._Refs;
 import org.apache.causeway.commons.internal.base._Refs.ObjectReference;
 import org.apache.causeway.core.config.presets.CausewayPresets;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
-import 
org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent;
 import org.apache.causeway.testdomain.conf.Configuration_usingJpa;
 import org.apache.causeway.testdomain.jpa.JpaTestDomainPersona;
 import org.apache.causeway.testdomain.jpa.entities.JpaBook;
diff --git 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java
 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java
index c89c965495..29b93681f6 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/conf/Configuration_headless.java
@@ -36,7 +36,6 @@ import 
org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.iactn.Interaction;
 import org.apache.causeway.applib.services.metrics.MetricsService;
 import org.apache.causeway.core.config.presets.CausewayPresets;
-import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
 import org.apache.causeway.security.bypass.CausewayModuleSecurityBypass;
 import 
org.apache.causeway.testdomain.util.interaction.DomainObjectTesterFactory;
diff --git 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java
 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java
index f279e3696f..46e344f5f2 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/publishing/PublishingTestFactoryAbstract.java
@@ -46,8 +46,6 @@ import org.apache.causeway.commons.internal.debug._Probe;
 import org.apache.causeway.commons.internal.debug.xray.XrayModel.Stickiness;
 import org.apache.causeway.commons.internal.debug.xray.XrayModel.ThreadMemento;
 import org.apache.causeway.commons.internal.debug.xray.XrayUi;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
-import 
org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent;
 
 import lombok.Getter;
 import lombok.NonNull;
diff --git 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java
 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java
index 4e5304f695..33c97b25e1 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/interaction/InteractionBoundaryProbe.java
@@ -29,9 +29,6 @@ import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.services.iactn.Interaction;
-import org.apache.causeway.core.interaction.scope.TransactionBoundaryAware;
-import 
org.apache.causeway.core.transaction.events.TransactionAfterCompletionEvent;
-import 
org.apache.causeway.core.transaction.events.TransactionBeforeCompletionEvent;
 import org.apache.causeway.testdomain.util.kv.KVStoreForTesting;
 
 import lombok.val;
diff --git a/scripts/ci/rename-all-published-sources-to-causeway.jsh 
b/scripts/ci/rename-all-published-sources-to-causeway.jsh
new file mode 100644
index 0000000000..6cde48b619
--- /dev/null
+++ b/scripts/ci/rename-all-published-sources-to-causeway.jsh
@@ -0,0 +1,380 @@
+/*
+ *  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.
+ */
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Renames project from oldName to newName.
+ *
+ * <p>
+ * This doesn't rewrite the contents of the following (binary) extensions:
+ * <ul>
+ *     <li>.jar</li>
+ *     <li>.zip</li>
+ *     <li>.pptx</li>
+ *     <li>.docx</li>
+ *     <li>.xlsx</li>
+ *     <li>.odt</li>
+ *     <li>.rtf</li>
+ *     <li>.pdf</li>
+ * </ul>
+ */
+class RenameProject {
+
+    static final boolean DRY_RUN = false;
+
+    // to obtain all the suffixes:
+    // find . -type f | sed -rn 's|.*/[^/]+\.([^/.]+)$|\1|p' | sort -u
+    static final List<String> EXTENSIONS = List.of(
+            "NOTICE",
+            "STATUS",
+            "MF",
+            "TXT",
+            // "adoc", // ignore adoc file content
+            "bat",
+            "cfg",
+            "css",
+            "dcl",
+            "dtd",
+            "factories",
+            "feature",
+            "fxml",
+            "gql",
+            "graphqls",
+            "hbs",
+            "html",
+            "importorder",
+            "info",
+            "ini",
+            "java",
+            "jdo",
+            "js",
+            "json",
+            "kt",
+            "kts",
+            "ldif",
+            "list",
+            "md",
+            "orm",
+            "po",
+            "pot",
+            "properties",
+            "puml",
+            "rdf",
+            "sh",
+            "soc",
+            "svg",
+            "template",
+            "thtml",
+            "ts",
+            "txt",
+            "xml",
+            "xsd",
+            "yaml",
+            "yml"
+            );
+
+    static final List<String> PATH_EXCLUSIONS = List.of(
+            "/build/",
+            "/target/",
+            "/adoc/", // not published in its legacy form
+            "/scripts/ci/", // don't touch
+            "/."
+            );
+
+    static final List<String> UNCONDITIONAL_INCLUSIONS = List.of(
+            "/META-INF/services/");
+
+    public static RenameProject renameBackToLegacy(final File root) {
+        return new RenameProject(root, "isis", "causeway");
+    }
+
+    private RenameProject(final File root, final String oldName, final String 
newName) {
+        this.root = root;
+        this.fromLower = oldName.toLowerCase();
+        this.toLower = newName.toLowerCase();
+        this.fromUpper = oldName.toUpperCase();
+        this.toUpper = newName.toUpperCase();
+        this.fromTitle = capitalize(fromLower);
+        this.toTitle = capitalize(toLower);
+    }
+
+    final File root;
+
+    final String fromLower;
+    final String toLower;
+    final String fromUpper;
+    final String toUpper;
+    final String fromTitle;
+    final String toTitle;
+
+    static String capitalize(final String s) { return s.substring(0, 
1).toUpperCase() + s.substring(1); }
+
+    public void renameAllFiles() throws IOException {
+        Files.find(root.toPath(), Integer.MAX_VALUE, (path, attr)->{
+            if(isPathExcluded(path.toFile())) {
+                return false;
+            }
+            return !attr.isDirectory();
+        }, FileVisitOption.FOLLOW_LINKS)
+        .map(Path::toFile)
+        //.filter(file->fileNameEndsWithSupportedExtension(file)) // rename 
files unconditionally
+        .forEach(file->{
+            renameIfRequired(file);
+        });
+    }
+
+    private void renameIfRequired(final File file) {
+        var relativeFilePathRenamed = pathRelativeToRoot(file)
+                .replace(fromTitle, toTitle)
+                .replace(fromUpper, toUpper)
+                .replace("\\" + fromLower, "\\" + toLower)
+                .replace("/" + fromLower, "/" + toLower)
+                .replace("-" + fromLower, "-" + toLower)
+                .replace("_" + fromLower, "_" + toLower)
+                .replace("." + fromLower, "." + toLower);
+
+        var filePathRenamed = pathOf(root) + relativeFilePathRenamed;
+
+        if (!filePathRenamed.equals(pathOf(file))) {
+
+            System.err.printf("rename %s -> %s%n", pathRelativeToRoot(file), 
relativeFilePathRenamed);
+            //System.err.printf("rename %s -> %s%n", pathOf(file), 
filePathRenamed);
+
+            if(!DRY_RUN) {
+                var fileRename = new File(filePathRenamed);
+                var parentFile = fileRename.getParentFile();
+                parentFile.mkdirs();
+                try {
+                    Files.move(file.toPath(), fileRename.toPath());
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    public void rewriteAllFileContents() throws IOException {
+
+        Files.find(root.toPath(), Integer.MAX_VALUE, (path, attr)->{
+            return !attr.isDirectory();
+        }, FileVisitOption.FOLLOW_LINKS)
+        .map(Path::toFile)
+        .filter(File::exists)
+        .forEach(file->{
+            if(isPathExcluded(file)) {
+                return;
+            }
+            if(isPathUnconditionallyIncluded(file)
+                    || fileNameEndsWithSupportedExtension(file)) {
+                try {
+                    rewriteIfRequired(file);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+    }
+
+    private void rewriteIfRequired(final File file) throws 
FileNotFoundException, IOException {
+        var lines = readLinesFromFile(file, StandardCharsets.UTF_8);
+        var newLines = new ArrayList<String>(lines.size());
+
+        final int linesChangedCount =
+            lines.stream().mapToInt(line->{
+                var newLine = line
+                        .replace(fromLower, toLower)
+                        .replace(fromUpper, toUpper)
+                        .replace(fromTitle, toTitle)
+
+                        // update schema declarations in .layout.xml files.
+                        // terrible hack - we are assuming the target is 
'causeway'
+                        // (also, the first arg has to handle the case that 
we've already converted replaced 'isis' -> 'causeway' throughout...)
+                        .replace(
+                                
"https://causeway.apache.org/applib/layout/menubars/bootstrap3 
https://causeway.apache.org/applib/layout/menubars/bootstrap3/menubars.xsd";,
+                                
"http://causeway.apache.org/applib/layout/menubars/bootstrap3 
https://isis.apache.org/applib/layout-v1/menubars/bootstrap3/menubars.xsd";)
+                        .replace(
+                                
"https://causeway.apache.org/applib/layout/component 
https://causeway.apache.org/applib/layout/component/component.xsd";,
+                                
"http://causeway.apache.org/applib/layout/component 
https://isis.apache.org/applib/layout-v1/component/component.xsd";)
+                        .replace(
+                                
"https://causeway.apache.org/applib/layout/grid/bootstrap3 
https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd";,
+                                
"http://causeway.apache.org/applib/layout/grid/bootstrap3 
https://isis.apache.org/applib/layout-v1/grid/bootstrap3/bootstrap3.xsd";)
+                        .replace(
+                                
"https://causeway.apache.org/applib/layout/links 
https://causeway.apache.org/applib/layout/links/links.xsd";,
+                                
"http://causeway.apache.org/applib/layout/links 
https://isis.apache.org/applib/layout-v1/links/links.xsd";)
+
+                        // update namespace declarations in all files 
(.layout.xml and also constants in .java classes)
+                        // (again, the first arg has to handle the case that 
we've already converted replaced 'isis' -> 'causeway' throughout...)
+                        .replace(
+                                
"\"https://causeway.apache.org/applib/layout/menubars/bootstrap3\"";,
+                                
"\"http://causeway.apache.org/applib/layout/menubars/bootstrap3\"";)
+                        .replace(
+                                
"\"https://causeway.apache.org/applib/layout/component\"";,
+                                
"\"http://causeway.apache.org/applib/layout/component\"";)
+                        .replace(
+                                
"\"https://causeway.apache.org/applib/layout/grid/bootstrap3\"";,
+                                
"\"http://causeway.apache.org/applib/layout/grid/bootstrap3\"";)
+                        .replace(
+                                
"\"https://causeway.apache.org/applib/layout/links";,
+                                
"\"http://causeway.apache.org/applib/layout/links";)
+                        .replace(
+                                "\"https://causeway.apache.org/schema";,
+                                "\"http://causeway.apache.org/schema";)
+
+                        ;
+                newLines.add(newLine);
+                return line.equals(newLine)
+                        ? 0
+                        : 1;
+            })
+            .sum();
+
+        if (linesChangedCount>0) {
+            System.err.printf("rewriting %s%n", pathRelativeToRoot(file));
+            if(!DRY_RUN) {
+                writeLinesToFile(newLines, file, StandardCharsets.UTF_8);
+            }
+        }
+    }
+
+    private static boolean fileNameEndsWithSupportedExtension(final File file) 
{
+        for (String ext : EXTENSIONS) {
+            if (file.getName().endsWith("." + ext)
+                    || file.getName().equals(ext)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isPathUnconditionallyIncluded(final File file) {
+        var path = pathOf(file);
+        for (String incl : UNCONDITIONAL_INCLUSIONS) {
+            if (path.contains(incl)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isPathExcluded(final File file) {
+        var path = pathOf(file);
+//        System.out.println(path);
+
+        for (String excl : PATH_EXCLUSIONS) {
+            if (path.contains(excl)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String pathRelativeToRoot(final File file) {
+        var prefix = pathOf(root);
+        var path = pathOf(file);
+        if(!path.startsWith(prefix)) {
+            throw new IllegalArgumentException("file not subpath of root");
+        }
+        return path.substring(prefix.length());
+    }
+
+    private static String pathOf(final File file) {
+        return file.getAbsolutePath().replace('\\', '/');
+    }
+
+    // -- READING
+
+    private static List<String> readLines(
+            final InputStream input,
+            final Charset charset){
+        if(input==null) {
+            return List.of();
+        }
+        var lines = new ArrayList<String>();
+        try(Scanner scanner = new Scanner(input, charset.name())){
+            scanner.useDelimiter("\\n");
+            while(scanner.hasNext()) {
+                var line = scanner.next();
+                //line = line.replace("\r", ""); // preserve windows specific 
line terminal
+                lines.add(line);
+            }
+        }
+        return lines;
+    }
+
+    private static List<String> readLinesFromFile(
+            final File file,
+            final Charset charset) throws FileNotFoundException, IOException {
+        try(var input = new FileInputStream(file)){
+            return readLines(input, charset);
+        }
+    }
+
+    // -- WRITING
+
+    static void writeLinesToFile(
+            final Iterable<String> lines,
+            final File file,
+            final Charset charset) throws FileNotFoundException, IOException {
+
+        try(var bw = new BufferedWriter(new OutputStreamWriter(new 
FileOutputStream(file), charset))) {
+            for(var line : lines) {
+                bw.append(line).append("\n");
+            }
+        }
+    }
+
+}
+
+boolean valid = true;
+
+String rootPath = System.getenv("ROOT_PATH_LEGACY");
+if(rootPath.isBlank() || ! new File(rootPath).exists()) {
+    System.err.println("env ROOT_PATH_LEGACY must point to an existing 
directory");
+    valid = false;
+}
+
+if (valid) {
+    var root = new File(rootPath);
+
+    var renamer = RenameProject.renameBackToLegacy(root);
+    System.out.printf("processing root %s%n", root.getAbsolutePath());
+
+    renamer.renameAllFiles();
+    renamer.rewriteAllFileContents();
+
+    System.out.println("done.");
+}
+
+
+/exit
diff --git 
a/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java
 
b/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java
index 7bcc65b6fb..0121ee3a48 100644
--- 
a/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java
+++ 
b/viewers/commons/services/src/main/java/org/apache/causeway/viewer/commons/services/branding/BrandingUiServiceDefault.java
@@ -43,8 +43,6 @@ implements BrandingUiService {
 
     @Inject
     public BrandingUiServiceDefault(final CausewayConfiguration 
causewayConfiguration) {
-        //TODO application name/logo borrowed from Wicket's configuration,
-        // we might generalize this config option to all viewers
         this.appConfig = 
causewayConfiguration.getViewer().getCommon().getApplication();
     }
 
diff --git 
a/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin
 
b/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin
deleted file mode 100644
index 055a88de82..0000000000
--- 
a/viewers/restfulobjects/applib/src/main/resources/META-INF/services/org.apache.causeway.viewer.restfulobjects.applib.client.UriBuilderPlugin
+++ /dev/null
@@ -1 +0,0 @@
-org.apache.causeway.viewer.restfulobjects.jaxrsresteasy4.IsisResteasy4Plugin
\ No newline at end of file
diff --git 
a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.java
 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.java
new file mode 100644
index 0000000000..215c8c4120
--- /dev/null
+++ 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV1.java
@@ -0,0 +1,108 @@
+/*
+ *  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.causeway.viewer.restfulobjects.rendering.service.conneg;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import org.apache.causeway.applib.annotation.PriorityPrecedence;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
+import 
org.apache.causeway.core.metamodel.interactions.managed.ManagedCollection;
+import org.apache.causeway.core.metamodel.interactions.managed.ManagedProperty;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import 
org.apache.causeway.viewer.restfulobjects.applib.CausewayModuleViewerRestfulObjectsApplib;
+import org.apache.causeway.viewer.restfulobjects.rendering.IResourceContext;
+import org.apache.causeway.viewer.restfulobjects.rendering.Responses;
+import 
org.apache.causeway.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
+
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * @since 1.x {@index}
+ */
+@Service
+@Named(ContentNegotiationServiceOrgApacheCausewayV1.LOGICAL_TYPE_NAME)
+@javax.annotation.Priority(PriorityPrecedence.MIDPOINT - 200)
+@Qualifier("OrgApacheCausewayV1")
+@Log4j2
+public class ContentNegotiationServiceOrgApacheCausewayV1 extends 
ContentNegotiationServiceAbstract {
+
+    static final String LOGICAL_TYPE_NAME = 
CausewayModuleViewerRestfulObjectsApplib.NAMESPACE + 
".ContentNegotiationServiceOrgApacheCausewayV1";
+    public static final String ACCEPT_PROFILE = "urn:org.apache.causeway/v1";
+
+    @Override
+    public Response.ResponseBuilder buildResponse(
+            final IResourceContext resourceContext,
+            final ManagedObject objectAdapter) {
+        return whenV1ThenNotImplemented(resourceContext);
+    }
+
+    @Override
+    public Response.ResponseBuilder buildResponse(
+            final IResourceContext resourceContext,
+            final ManagedProperty objectAndProperty)  {
+        return whenV1ThenNotImplemented(resourceContext);
+    }
+
+    @Override
+    public Response.ResponseBuilder buildResponse(
+            final IResourceContext resourceContext,
+            final ManagedCollection managedCollection) {
+        return whenV1ThenNotImplemented(resourceContext);
+    }
+
+    @Override
+    public Response.ResponseBuilder buildResponse(
+            final IResourceContext resourceContext,
+            final ManagedAction objectAndAction)  {
+        return whenV1ThenNotImplemented(resourceContext);
+    }
+
+    @Override
+    public Response.ResponseBuilder buildResponse(
+            final IResourceContext resourceContext,
+            final ObjectAndActionInvocation objectAndActionInvocation) {
+        return whenV1ThenNotImplemented(resourceContext);
+    }
+
+    // -- HELPER
+
+    private boolean canAccept(final IResourceContext resourceContext) {
+        final List<MediaType> acceptableMediaTypes = 
resourceContext.getAcceptableMediaTypes();
+        return mediaTypeParameterMatches(acceptableMediaTypes, "profile", 
ACCEPT_PROFILE);
+    }
+
+    private ResponseBuilder whenV1ThenNotImplemented(final IResourceContext 
resourceContext) {
+        if(!canAccept(resourceContext)) {
+            return null;
+        }
+        log.warn("profile '{}' is no longer supported use '{}' instead",
+                ACCEPT_PROFILE,
+                ContentNegotiationServiceOrgApacheCausewayV2.ACCEPT_PROFILE);
+        return Responses.ofNotImplemented();
+    }
+
+}
diff --git a/viewers/restfulobjects/viewer/src/main/java/module-info.java 
b/viewers/restfulobjects/viewer/src/main/java/module-info.java
index 2963adccec..cd5dfa686d 100644
--- a/viewers/restfulobjects/viewer/src/main/java/module-info.java
+++ b/viewers/restfulobjects/viewer/src/main/java/module-info.java
@@ -52,5 +52,4 @@ module org.apache.causeway.viewer.restfulobjects.viewer {
     requires spring.context;
     requires spring.core;
     requires spring.web;
-    requires spring.boot;
-}
\ No newline at end of file
+}

Reply via email to