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

ahuber pushed a commit to branch 3937-grid.api.overhaul
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 012be077069e5bdc37f6feff15c2c5ed9ea1fd13
Author: Andi Huber <[email protected]>
AuthorDate: Mon Oct 27 16:06:29 2025 +0100

    CAUSEWAY-2297: rename, move and encapsulate
---
 .../applib/services/grid/GridLoaderService.java    |  89 -----
 .../applib/services/grid/GridMarshaller.java       |   5 +-
 .../causeway/applib/services/grid/GridService.java |  23 +-
 .../applib/services/grid/GridSystemService.java    |  10 +-
 core/metamodel/src/main/java/module-info.java      |   1 -
 .../metamodel/CausewayModuleCoreMetamodel.java     |  10 +-
 .../core/metamodel/services/grid/GridCache.java    | 190 ++++++++++
 .../grid/{bootstrap => }/GridFallbackLayout.xml    |   0
 .../{bootstrap => }/GridInitializationModel.java   |   2 +-
 .../core/metamodel/services/grid/GridLoader.java   | 107 ++++++
 .../services/grid/GridLoaderServiceDefault.java    | 244 -------------
 ...erviceBootstrap.java => GridMarshallerXml.java} |  16 +-
 .../services/grid/GridServiceDefault.java          |  29 +-
 .../services/grid/GridSystemServiceAbstract.java   | 404 ---------------------
 .../GridSystemServiceBootstrap.java                | 360 +++++++++++++++++-
 .../grid/XsiSchemaLocationProviderForGrid.java     |   2 +-
 .../{bootstrap => }/_UnreferencedSequenceUtil.java |   2 +-
 .../services/grid/spi/LayoutResource.java          |   4 +-
 .../services/grid/spi/LayoutResourceLoader.java    |  14 +-
 .../grid/spi/LayoutResourceLoaderDefault.java      |   7 +-
 ...meTest.java => GridCache_resourceNameTest.java} |  24 +-
 .../metamodel/services/grid/GridLoadingTest.java   |  27 +-
 .../mmtestsupport/MetaModelContext_forTesting.java |  22 +-
 23 files changed, 736 insertions(+), 856 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridLoaderService.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridLoaderService.java
deleted file mode 100644
index bd892fb882e..00000000000
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridLoaderService.java
+++ /dev/null
@@ -1,89 +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.applib.services.grid;
-
-import java.util.EnumSet;
-import java.util.Optional;
-
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
-import org.apache.causeway.applib.mixins.metamodel.Object_rebuildMetamodel;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-
-/**
- * Loads the XML layout (grid) for a domain class.
- *
- * @since 1.x - revised for 2.0 {@index}
- */
-public interface GridLoaderService {
-
-    /**
-     * Whether dynamic reloading of layouts is enabled.
-     *
-     * <p> The default implementation enables reloading for prototyping mode,
-     * disables in production
-     */
-    boolean supportsReloading();
-
-    /**
-     * To support metamodel invalidation/rebuilding of spec.
-     *
-     * <p>This is called by the {@link Object_rebuildMetamodel} mixin action.
-     */
-    void remove(Class<?> domainClass);
-
-    /**
-     * Whether any persisted layout metadata (eg a <code>.layout.xml</code> 
file) exists for this domain class.
-     *
-     * <p>If none exists, will return null (and the calling {@link 
GridService}  will use {@link GridSystemService}
-     * to obtain a default grid for the domain class).
-     */
-    boolean existsFor(Class<?> domainClass, EnumSet<CommonMimeType> 
supportedFormats);
-
-    /**
-     * Optionally returns a new instance of a {@link BSGrid},
-     * based on whether the underlying resource could be found, loaded and 
parsed.
-     *
-     * <p>The layout alternative will typically be specified through a
-     * `layout()` method on the domain object, the value of which is used
-     * for the suffix of the layout file (eg "Customer-layout.archived.xml"
-     * to use a different layout for customers that have been archived).
-     *
-     * @throws UnsupportedOperationException - when format is not supported
-     */
-    Optional<BSGrid> load(
-            Class<?> domainClass,
-            @Nullable String layoutIfAny,
-            @NonNull GridMarshaller marshaller);
-
-    /**
-     * Optionally returns a new instance of a {@link BSGrid},
-     * based on whether the underlying resource could be found, loaded and 
parsed.
-     *
-     * @throws UnsupportedOperationException - when format is not supported
-     */
-    default Optional<BSGrid> load(
-            final Class<?> domainClass,
-            final @NonNull GridMarshaller marshaller) {
-        return load(domainClass, null, marshaller);
-    }
-
-}
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridMarshaller.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridMarshaller.java
index 1fa32a5234f..2ffb317d705 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridMarshaller.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridMarshaller.java
@@ -24,6 +24,7 @@
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
+import org.apache.causeway.applib.services.layout.LayoutService;
 import org.apache.causeway.applib.services.marshal.Marshaller;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.causeway.commons.functional.Try;
@@ -37,11 +38,11 @@
  */
 public interface GridMarshaller {
 
-    Class<BSGrid> supportedClass();
-
     /**
      * Supported format(s) for {@link #unmarshal(Class, String, 
CommonMimeType)}
      * and {@link #marshal(BSGrid, CommonMimeType)}.
+     *
+     * @apiNote also used by {@link LayoutService} to lookup different formats
      */
     EnumSet<CommonMimeType> supportedFormats();
 
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
index 2be3e9d9046..df396a12c1e 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridService.java
@@ -29,33 +29,26 @@
 /**
  * Loads the layout (grid) for any domain class.
  *
- * <p> Acts on top of {@link GridLoaderService}, {@link GridMarshaller} and 
any {@link GridSystemService}(s) registered with Spring.
- * @since 1.x {@index}
+ * <p> Acts on top of {@link GridMarshaller} and any {@link 
GridSystemService}(s) registered with Spring.
+ *
+ * @since 1.x revised for 4.0 {@index}
  */
 public interface GridService {
 
     /**
      * Whether dynamic reloading of layouts is enabled.
      *
-     * <p> The default implementation just delegates to the configured
-     * {@link GridLoaderService}; the default implementation of <i>that</i>
-     * service enables reloading while prototyping, disables in production.
+     * <p> The default implementation enables reloading while prototyping, 
disables in production.
      */
     boolean supportsReloading();
 
     /**
      * To support metamodel invalidation/rebuilding of spec.
-     *
-     * <p>The default implementation just delegates to the configured
-     * {@link GridLoaderService}.
      */
     void remove(Class<?> domainClass);
 
     /**
      * Whether any persisted layout metadata (eg a <code>.layout.xml</code> 
file) exists for this domain class.
-     *
-     * <p>The default implementation just delegates to the configured
-     * {@link GridLoaderService}.
      */
     boolean existsFor(Class<?> domainClass);
 
@@ -67,8 +60,6 @@ public interface GridService {
      * use {@link GridService#defaultGridFor(Class)} to obtain a
      * default grid if necessary).
      *
-     * <p>The default implementation just delegates to the configured
-     * {@link GridLoaderService}.
      */
     BSGrid load(final Class<?> domainClass);
 
@@ -79,9 +70,7 @@ public interface GridService {
      * domain object's <code>layout()</code> method, whereby - based on the
      * state of the domain object - it requests a different layout be used.
      *
-     * <p>The default implementation just delegates to the configured
-     * {@link GridLoaderService}; the default implementation of <i>that</i>
-     * service uses the layout name to search for a differently
+     * <p>The default implementation uses the layout name to search for a 
differently
      * named layout file, <code>[domainClass].layout.[layout].xml</code>.
      */
     BSGrid load(Class<?> domainClass, String layout);
@@ -134,7 +123,7 @@ public interface GridService {
 
     // -- LAYOUT EXPORT
 
-    GridMarshaller marshaller(); //TODO rename
+    GridMarshaller marshaller();
 
     default BSGrid toGridForExport(
             final Class<?> domainClass,
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridSystemService.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridSystemService.java
index 2a0ec6532b1..bef92c7cc4e 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridSystemService.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/grid/GridSystemService.java
@@ -29,18 +29,10 @@
  * of the grid) and in providing a default grid if there is no other metadata
  * available.
  *
- * @since 1.x {@index}
+ * @since 1.x revised for 4.0 {@index}
  */
 public interface GridSystemService {
 
-    /**
-     * The concrete subclass of {@link BSGrid} supported by this 
implementation.
-     *
-     * <p>There can be multiple implementations of this service, this indicates
-     * the base class used by the implementation.
-     */
-    Class<BSGrid> gridImplementation();
-
     /**
      * A default grid, used when no grid layout can be found for the domain
      * class.
diff --git a/core/metamodel/src/main/java/module-info.java 
b/core/metamodel/src/main/java/module-info.java
index bce3eed5164..49ed9a4f34a 100644
--- a/core/metamodel/src/main/java/module-info.java
+++ b/core/metamodel/src/main/java/module-info.java
@@ -97,7 +97,6 @@
     exports org.apache.causeway.core.metamodel.services.devutils;
     exports org.apache.causeway.core.metamodel.services.events;
     exports org.apache.causeway.core.metamodel.services.exceprecog;
-    exports org.apache.causeway.core.metamodel.services.grid.bootstrap;
     exports org.apache.causeway.core.metamodel.services.grid;
     exports org.apache.causeway.core.metamodel.services.idstringifier;
     exports org.apache.causeway.core.metamodel.services.ixn;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
index eff931b9e05..8231c1b4ebb 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/CausewayModuleCoreMetamodel.java
@@ -48,11 +48,9 @@
 import 
org.apache.causeway.core.metamodel.services.columnorder.ColumnOrderTxtFileServiceDefault;
 import 
org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
 import 
org.apache.causeway.core.metamodel.services.exceprecog.ExceptionRecognizerForRecoverableException;
-import 
org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault;
+import org.apache.causeway.core.metamodel.services.grid.GridMarshallerXml;
 import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault;
-import 
org.apache.causeway.core.metamodel.services.grid.XsiSchemaLocationProviderForGrid;
-import 
org.apache.causeway.core.metamodel.services.grid.bootstrap.GridMarshallerServiceBootstrap;
-import 
org.apache.causeway.core.metamodel.services.grid.bootstrap.GridSystemServiceBootstrap;
+import 
org.apache.causeway.core.metamodel.services.grid.GridSystemServiceBootstrap;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
 import 
org.apache.causeway.core.metamodel.services.idstringifier.IdStringifierLookupService;
 import 
org.apache.causeway.core.metamodel.services.inject.ServiceInjectorDefault;
@@ -173,9 +171,7 @@
         // @Service's
         ColumnOrderTxtFileServiceDefault.class,
         ExceptionRecognizerForRecoverableException.class,
-        XsiSchemaLocationProviderForGrid.class,
-        GridLoaderServiceDefault.class,
-        GridMarshallerServiceBootstrap.class,
+        GridMarshallerXml.class,
         GridServiceDefault.class,
         GridSystemServiceBootstrap.class,
         IdStringifierLookupService.class,
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
new file mode 100644
index 00000000000..e75c2a80565
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridCache.java
@@ -0,0 +1,190 @@
+/*
+ *  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.metamodel.services.grid;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.jspecify.annotations.NonNull;
+
+import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
+import org.apache.causeway.applib.mixins.metamodel.Object_rebuildMetamodel;
+import org.apache.causeway.applib.services.grid.GridMarshaller;
+import org.apache.causeway.applib.services.grid.GridService;
+import org.apache.causeway.applib.services.grid.GridSystemService;
+import org.apache.causeway.applib.services.message.MessageService;
+import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.services.grid.GridLoader.LayoutKey;
+import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Cache for {@link BSGrid} instances,
+ * delegating grid loading to the {@link GridLoader}.
+ *
+ * @since 4.0
+ */
+@Slf4j
+record GridCache(
+    GridLoader gridLoader,
+    MessageService messageService,
+    /**
+     * Whether dynamic reloading of layouts is enabled.
+     *
+     * <p> The default implementation enables reloading for prototyping mode,
+     * disables in production
+     */
+    boolean supportsReloading,
+    Map<LayoutKey, BSGrid> gridCache,
+    // for better logging messages (used only in prototyping mode)
+    Map<LayoutKey, String> badContentByKey) {
+
+    public GridCache(
+            final MessageService messageService,
+            final boolean supportsReloading,
+            final List<LayoutResourceLoader> layoutResourceLoaders) {
+        this(new GridLoader(Can.ofCollection(layoutResourceLoaders)),
+            messageService,
+            supportsReloading,
+            new HashMap<>(), new HashMap<>());
+    }
+
+    /**
+     * To support metamodel invalidation/rebuilding of spec.
+     * Acts as a no-op if reloading is not supported.
+     *
+     * <p>This is called by the {@link Object_rebuildMetamodel} mixin action.
+     */
+    public void remove(final Class<?> domainClass) {
+        if(!supportsReloading()) return;
+
+        final String layoutIfAny = null;
+        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
+        badContentByKey.remove(layoutKey);
+        gridCache.remove(layoutKey);
+    }
+
+    /**
+     * Whether any persisted layout metadata (eg a <code>.layout.xml</code> 
file) exists for this domain class.
+     *
+     * <p>If none exists, will return null (and the calling {@link 
GridService} will use {@link GridSystemService}
+     * to obtain a default grid for the domain class).
+     */
+    public boolean existsFor(final Class<?> domainClass, final 
EnumSet<CommonMimeType> supportedFormats) {
+        return gridLoader.loadLayoutResource(new LayoutKey(domainClass, null), 
supportedFormats).isPresent();
+    }
+
+    /**
+     * Optionally returns a new instance of a {@link BSGrid},
+     * based on whether the underlying resource could be found, loaded and 
parsed.
+     *
+     * <p>The layout alternative will typically be specified through a
+     * `layout()` method on the domain object, the value of which is used
+     * for the suffix of the layout file (eg "Customer-layout.archived.xml"
+     * to use a different layout for customers that have been archived).
+     *
+     * @throws UnsupportedOperationException - when format is not supported
+     */
+    public Optional<BSGrid> load(
+            final Class<?> domainClass,
+            final String layoutIfAny,
+            final @NonNull GridMarshaller marshaller) {
+
+        var supportedFormats = marshaller.supportedFormats();
+
+        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
+        var layoutResource = gridLoader.loadLayoutResource(layoutKey, 
supportedFormats).orElse(null);
+        if(layoutResource == null) {
+            log.debug(
+                    "Failed to locate or load layout resource for class {}, "
+                    + "with layout-suffix (if any) {}, "
+                    + "using layout-resource-loaders {}.",
+                    domainClass.getName(), layoutIfAny,
+                    gridLoader().layoutResourceLoaders().stream()
+                        .map(Object::getClass)
+                        .map(Class::getName)
+                        .collect(Collectors.joining(", ")));
+            return Optional.empty();
+        }
+
+        if(supportsReloading()) {
+            final String badContent = badContentByKey.get(layoutKey);
+            if(badContent != null) {
+                if(Objects.equals(layoutResource.content(), badContent)) {
+                    // seen this before and already logged; just quit
+                    return Optional.empty();
+                } else {
+                    // this different content might be good
+                    badContentByKey.remove(layoutKey);
+                }
+            }
+        } else {
+            // if cached, serve from cache - otherwise fall through
+            final BSGrid grid = gridCache.get(layoutKey);
+            if(grid != null) return Optional.of(grid);
+        }
+
+        try {
+            final BSGrid grid = marshaller
+                .unmarshal(domainClass, layoutResource.content(), 
layoutResource.format())
+                .getValue().orElseThrow();
+            if(supportsReloading()) {
+                gridCache.put(layoutKey, grid);
+            }
+            return Optional.of(grid);
+        } catch(Exception ex) {
+
+            if(supportsReloading()) {
+                // save fact that this was bad content, so that we don't log 
again if called next time
+                badContentByKey.put(layoutKey, layoutResource.content());
+            }
+
+            // note that we don't blacklist if the file exists but couldn't be 
parsed;
+            // the developer might fix so we will want to retry.
+            final String resourceName = layoutResource.resourceName();
+            final String message = "Failed to parse " + resourceName + " file 
(" + ex.getMessage() + ")";
+            if(supportsReloading()) {
+                messageService.warnUser(message);
+            }
+            log.warn(message);
+
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Optionally returns a new instance of a {@link BSGrid},
+     * based on whether the underlying resource could be found, loaded and 
parsed.
+     *
+     * @throws UnsupportedOperationException - when format is not supported
+     */
+    public Optional<BSGrid> load(
+            final Class<?> domainClass,
+            final @NonNull GridMarshaller marshaller) {
+        return load(domainClass, null, marshaller);
+    }
+
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridFallbackLayout.xml
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridFallbackLayout.xml
similarity index 100%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridFallbackLayout.xml
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridFallbackLayout.xml
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridInitializationModel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridInitializationModel.java
similarity index 99%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridInitializationModel.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridInitializationModel.java
index 7f88800c6c3..92bed33d96a 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridInitializationModel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridInitializationModel.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.bootstrap;
+package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.Collection;
 import java.util.LinkedHashMap;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
new file mode 100644
index 00000000000..094fe1122ad
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoader.java
@@ -0,0 +1,107 @@
+/*
+ *  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.metamodel.services.grid;
+
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.reflection._Reflect;
+import 
org.apache.causeway.commons.internal.reflection._Reflect.InterfacePolicy;
+import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
+import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
+
+record GridLoader(
+        Can<LayoutResourceLoader> layoutResourceLoaders) {
+
+    public record LayoutKey(
+        @NonNull Class<?> domainClass,
+        /** layout suffix */
+        @Nullable String layoutIfAny) {
+    }
+
+    // -- HELPER
+
+    Optional<LayoutResource> loadLayoutResource(
+            final LayoutKey layoutKey,
+            final EnumSet<CommonMimeType> supportedFormats) {
+        return _Reflect.streamTypeHierarchy(layoutKey.domainClass(), 
InterfacePolicy.EXCLUDE)
+            .flatMap(type->loadContent(type, layoutKey.layoutIfAny(), 
supportedFormats).stream())
+            .findFirst();
+    }
+
+    private Optional<LayoutResource> loadContent(
+            final @NonNull Class<?> domainClass,
+            final @Nullable String layoutIfAny,
+            final EnumSet<CommonMimeType> supportedFormats) {
+        return streamResourceNameCandidatesFor(domainClass, layoutIfAny, 
supportedFormats)
+            
.flatMap(candidateResourceName->lookupLayoutResourceUsingLoaders(domainClass, 
candidateResourceName).stream())
+            .findFirst();
+    }
+
+    private Stream<String> streamResourceNameCandidatesFor(
+            final @NonNull Class<?> domainClass,
+            final @Nullable String layoutIfAny,
+            final @NonNull  EnumSet<CommonMimeType> supportedFormats) {
+        return supportedFormats.stream()
+                .flatMap(format->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, format));
+    }
+
+    private Stream<String> streamResourceNameCandidatesFor(
+            final @NonNull Class<?> domainClass,
+            final @Nullable String layoutIfAny,
+            final @NonNull CommonMimeType format) {
+        return format.proposedFileExtensions().stream()
+                
.flatMap(fileExtension->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, fileExtension));
+    }
+
+    private Stream<String> streamResourceNameCandidatesFor(
+            final @NonNull Class<?> domainClass,
+            final @Nullable String layoutIfAny,
+            final @NonNull String fileExtension) {
+
+        var typeSimpleName = domainClass.getSimpleName();
+
+        return _Strings.isNotEmpty(layoutIfAny)
+                ? Stream.of(
+                        String.format("%s-%s.layout.%s", typeSimpleName, 
layoutIfAny, fileExtension),
+                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
+                        String.format("%s.layout.fallback.%s", typeSimpleName, 
fileExtension))
+                : Stream.of(
+                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
+                        String.format("%s.layout.fallback.%s", 
typeSimpleName,fileExtension));
+    }
+
+    private Optional<LayoutResource> lookupLayoutResourceUsingLoaders(
+            final @NonNull Class<?> type,
+            final @NonNull String candidateResourceName) {
+
+        return layoutResourceLoaders.stream()
+            .flatMap(loader->loader.lookupLayoutResource(type, 
candidateResourceName).stream())
+            .findFirst();
+    }
+
+
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault.java
deleted file mode 100644
index 599495ade14..00000000000
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault.java
+++ /dev/null
@@ -1,244 +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.metamodel.services.grid;
-
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import jakarta.annotation.Priority;
-import jakarta.inject.Inject;
-import jakarta.inject.Named;
-
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
-import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
-import org.apache.causeway.applib.services.grid.GridLoaderService;
-import org.apache.causeway.applib.services.grid.GridMarshaller;
-import org.apache.causeway.applib.services.message.MessageService;
-import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Maps;
-import org.apache.causeway.commons.internal.reflection._Reflect;
-import 
org.apache.causeway.commons.internal.reflection._Reflect.InterfacePolicy;
-import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
-import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
-import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
-import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import lombok.experimental.Accessors;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * Default implementation of {@link GridLoaderService}.
- *
- * @since 1.x revised for 2.0 {@index}
- */
-@Service
-@Named(CausewayModuleCoreMetamodel.NAMESPACE + ".GridLoaderServiceDefault")
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
-@RequiredArgsConstructor //JUnit Support
-@Slf4j
-public class GridLoaderServiceDefault implements GridLoaderService {
-
-    private final MessageService messageService;
-    final Can<LayoutResourceLoader> layoutResourceLoaders;
-
-    @Getter(onMethod_={@Override}) @Accessors(fluent = true)
-    private final boolean supportsReloading;
-
-    @Inject
-    public GridLoaderServiceDefault(
-            final MessageService messageService,
-            final CausewaySystemEnvironment causewaySystemEnvironment,
-            final List<LayoutResourceLoader> layoutResourceLoaders) {
-        this.messageService = messageService;
-        this.supportsReloading = causewaySystemEnvironment.isPrototyping();
-        this.layoutResourceLoaders = Can.ofCollection(layoutResourceLoaders);
-    }
-
-    record LayoutKey(
-            @NonNull Class<?> domainClass,
-            /** layout suffix */
-            @Nullable String layoutIfAny) {
-    }
-
-    // for better logging messages (used only in prototyping mode)
-    private final Map<LayoutKey, String> badContentByKey = _Maps.newHashMap();
-    // cache (used only in prototyping mode)
-    private final Map<LayoutKey, BSGrid> gridCache = _Maps.newHashMap();
-
-    @Override
-    public void remove(final Class<?> domainClass) {
-        if(!supportsReloading()) {
-            return;
-        }
-        final String layoutIfAny = null;
-        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
-        badContentByKey.remove(layoutKey);
-        gridCache.remove(layoutKey);
-    }
-
-    @Override
-    public boolean existsFor(final Class<?> domainClass, final 
EnumSet<CommonMimeType> supportedFormats) {
-        return loadLayoutResource(new LayoutKey(domainClass, null), 
supportedFormats).isPresent();
-    }
-
-    @Override
-    public Optional<BSGrid> load(
-            final Class<?> domainClass,
-            final String layoutIfAny,
-            final @NonNull GridMarshaller marshaller) {
-
-        var supportedFormats = marshaller.supportedFormats();
-
-        var layoutKey = new LayoutKey(domainClass, layoutIfAny);
-        var layoutResource = loadLayoutResource(layoutKey, 
supportedFormats).orElse(null);
-        if(layoutResource == null) {
-            log.debug(
-                    "Failed to locate or load layout resource for class {}, "
-                    + "with layout-suffix (if any) {}, "
-                    + "using layout-resource-loaders {}.",
-                    domainClass.getName(), layoutIfAny,
-                    
layoutResourceLoaders.stream().map(Object::getClass).map(Class::getName).collect(Collectors.joining(",
 ")));
-            return Optional.empty();
-        }
-
-        if(supportsReloading()) {
-            final String badContent = badContentByKey.get(layoutKey);
-            if(badContent != null) {
-                if(Objects.equals(layoutResource.content(), badContent)) {
-                    // seen this before and already logged; just quit
-                    return Optional.empty();
-                } else {
-                    // this different content might be good
-                    badContentByKey.remove(layoutKey);
-                }
-            }
-        } else {
-            // if cached, serve from cache - otherwise fall through
-            final BSGrid grid = gridCache.get(layoutKey);
-            if(grid != null) {
-                return Optional.of(grid);
-            }
-        }
-
-        try {
-            final BSGrid grid = marshaller
-                .unmarshal(domainClass, layoutResource.content(), 
layoutResource.format())
-                .getValue().orElseThrow();
-            if(supportsReloading()) {
-                gridCache.put(layoutKey, grid);
-            }
-            return Optional.of(grid);
-        } catch(Exception ex) {
-
-            if(supportsReloading()) {
-                // save fact that this was bad content, so that we don't log 
again if called next time
-                badContentByKey.put(layoutKey, layoutResource.content());
-            }
-
-            // note that we don't blacklist if the file exists but couldn't be 
parsed;
-            // the developer might fix so we will want to retry.
-            final String resourceName = layoutResource.resourceName();
-            final String message = "Failed to parse " + resourceName + " file 
(" + ex.getMessage() + ")";
-            if(supportsReloading()) {
-                messageService.warnUser(message);
-            }
-            log.warn(message);
-
-            return Optional.empty();
-        }
-    }
-
-    // -- HELPER
-
-    Optional<LayoutResource> loadLayoutResource(
-            final LayoutKey layoutKey,
-            final EnumSet<CommonMimeType> supportedFormats) {
-        return _Reflect.streamTypeHierarchy(layoutKey.domainClass(), 
InterfacePolicy.EXCLUDE)
-            .flatMap(type->loadContent(type, layoutKey.layoutIfAny(), 
supportedFormats).stream())
-            .findFirst();
-    }
-
-    private Optional<LayoutResource> loadContent(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final EnumSet<CommonMimeType> supportedFormats) {
-        return streamResourceNameCandidatesFor(domainClass, layoutIfAny, 
supportedFormats)
-            
.flatMap(candidateResourceName->lookupLayoutResourceUsingLoaders(domainClass, 
candidateResourceName).stream())
-            .findFirst();
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull  EnumSet<CommonMimeType> supportedFormats) {
-        return supportedFormats.stream()
-                .flatMap(format->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, format));
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull CommonMimeType format) {
-        return format.proposedFileExtensions().stream()
-                
.flatMap(fileExtension->streamResourceNameCandidatesFor(domainClass, 
layoutIfAny, fileExtension));
-    }
-
-    private Stream<String> streamResourceNameCandidatesFor(
-            final @NonNull Class<?> domainClass,
-            final @Nullable String layoutIfAny,
-            final @NonNull String fileExtension) {
-
-        var typeSimpleName = domainClass.getSimpleName();
-
-        return _Strings.isNotEmpty(layoutIfAny)
-                ? Stream.of(
-                        String.format("%s-%s.layout.%s", typeSimpleName, 
layoutIfAny, fileExtension),
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", typeSimpleName, 
fileExtension))
-                : Stream.of(
-                        String.format("%s.layout.%s", typeSimpleName, 
fileExtension),
-                        String.format("%s.layout.fallback.%s", 
typeSimpleName,fileExtension));
-    }
-
-    private Optional<LayoutResource> lookupLayoutResourceUsingLoaders(
-            final @NonNull Class<?> type,
-            final @NonNull String candidateResourceName) {
-
-        return layoutResourceLoaders.stream()
-            .flatMap(loader->loader.lookupLayoutResource(type, 
candidateResourceName).stream())
-            .findFirst();
-    }
-
-}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridMarshallerServiceBootstrap.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridMarshallerXml.java
similarity index 86%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridMarshallerServiceBootstrap.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridMarshallerXml.java
index 36fd1f48a59..d2ef96040e3 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridMarshallerServiceBootstrap.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridMarshallerXml.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.bootstrap;
+package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.EnumSet;
 import java.util.Map;
@@ -42,7 +42,6 @@
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.commons.io.JaxbUtils;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
-import 
org.apache.causeway.core.metamodel.services.grid.XsiSchemaLocationProviderForGrid;
 
 /**
  * Default implementation of {@link GridMarshaller} using DTOs based on
@@ -51,18 +50,18 @@
  * @since 2.0 {@index}
  */
 @Service
-@Named(CausewayModuleCoreMetamodel.NAMESPACE + 
".GridMarshallerServiceBootstrap")
+@Named(CausewayModuleCoreMetamodel.NAMESPACE + ".GridMarshallerXml")
 @Priority(PriorityPrecedence.MIDPOINT)
 @Qualifier("Default")
-public record GridMarshallerServiceBootstrap(
+public record GridMarshallerXml(
         JaxbService jaxbService,
         XsiSchemaLocationProviderForGrid schemaLocationProvider,
         EnumSet<CommonMimeType> supportedFormats
     ) implements GridMarshaller {
 
     @Inject
-    public GridMarshallerServiceBootstrap(final JaxbService jaxbService, final 
XsiSchemaLocationProviderForGrid schemaLocationProvider) {
-        this(jaxbService, schemaLocationProvider, 
EnumSet.of(CommonMimeType.XML));
+    public GridMarshallerXml(final JaxbService jaxbService) {
+        this(jaxbService, new XsiSchemaLocationProviderForGrid(), 
EnumSet.of(CommonMimeType.XML));
         // eagerly create a JAXBContext for this grid type (and cache it)
         JaxbUtils.jaxbContextFor(BSGrid.class, true);
     }
@@ -72,11 +71,6 @@ public EnumSet<CommonMimeType> supportedFormats() {
         return supportedFormats;
     }
 
-    @Override
-    public Class<BSGrid> supportedClass() {
-        return BSGrid.class;
-    }
-
     @Override
     public String marshal(final @NonNull BSGrid bsGrid, final @NonNull 
CommonMimeType format) {
         throwIfFormatNotSupported(format);
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
index 00a234e5eae..55e64297794 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridServiceDefault.java
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import jakarta.annotation.Priority;
+import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -28,12 +29,14 @@
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
-import org.apache.causeway.applib.services.grid.GridLoaderService;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.grid.GridService;
 import org.apache.causeway.applib.services.grid.GridSystemService;
+import org.apache.causeway.applib.services.message.MessageService;
 import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
+import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 
 /**
  * Default implementation of {@link GridService}.
@@ -45,33 +48,43 @@
 @Priority(PriorityPrecedence.MIDPOINT)
 @Qualifier("Default")
 public record GridServiceDefault(
-    GridLoaderService gridLoaderService,
     GridMarshaller marshaller,
-    List<GridSystemService> gridSystemServices) implements GridService {
+    List<GridSystemService> gridSystemServices,
+    GridCache gridCache) implements GridService {
+
+    @Inject
+    public GridServiceDefault(
+            final CausewaySystemEnvironment causewaySystemEnvironment,
+            final GridMarshaller marshaller,
+            final MessageService messageService,
+            final List<GridSystemService> gridSystemServices,
+            final List<LayoutResourceLoader> layoutResourceLoaders) {
+        this(marshaller, gridSystemServices, new GridCache(messageService, 
causewaySystemEnvironment.isPrototyping(), layoutResourceLoaders));
+    }
 
     @Override
     public boolean supportsReloading() {
-        return gridLoaderService.supportsReloading();
+        return gridCache.supportsReloading();
     }
 
     @Override
     public void remove(final Class<?> domainClass) {
-        gridLoaderService.remove(domainClass);
+        gridCache.remove(domainClass);
     }
 
     @Override
     public boolean existsFor(final Class<?> domainClass) {
-        return gridLoaderService.existsFor(domainClass, 
marshaller.supportedFormats());
+        return gridCache.existsFor(domainClass, marshaller.supportedFormats());
     }
 
     @Override
     public BSGrid load(final Class<?> domainClass) {
-        return gridLoaderService.load(domainClass, marshaller).orElse(null);
+        return gridCache.load(domainClass, marshaller).orElse(null);
     }
 
     @Override
     public BSGrid load(final Class<?> domainClass, final String layout) {
-        return gridLoaderService.load(domainClass, layout, 
marshaller).orElse(null);
+        return gridCache.load(domainClass, layout, marshaller).orElse(null);
     }
 
     // --
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceAbstract.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceAbstract.java
deleted file mode 100644
index 82f89a3c5d7..00000000000
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceAbstract.java
+++ /dev/null
@@ -1,404 +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.metamodel.services.grid;
-
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import jakarta.inject.Inject;
-import jakarta.inject.Provider;
-
-import org.apache.causeway.applib.annotation.Programmatic;
-import org.apache.causeway.applib.layout.component.ActionLayoutData;
-import org.apache.causeway.applib.layout.component.CollectionLayoutData;
-import org.apache.causeway.applib.layout.component.DomainObjectLayoutData;
-import org.apache.causeway.applib.layout.component.DomainObjectLayoutDataOwner;
-import org.apache.causeway.applib.layout.component.FieldSet;
-import org.apache.causeway.applib.layout.component.PropertyLayoutData;
-import 
org.apache.causeway.applib.layout.grid.bootstrap.BSElement.BSElementVisitor;
-import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
-import org.apache.causeway.applib.services.grid.GridSystemService;
-import org.apache.causeway.applib.services.i18n.TranslationService;
-import org.apache.causeway.applib.services.message.MessageService;
-import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
-import org.apache.causeway.core.metamodel.facetapi.Facet;
-import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.ActionPositionFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.CssClassFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.FaFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.HiddenFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.MemberDescribedFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.actions.layout.MemberNamedFacetForActionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.CssClassFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.DefaultViewFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.HiddenFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.MemberDescribedFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.MemberNamedFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.PagedFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.SortedByFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.collections.layout.tabledec.TableDecoratorFacetForCollectionLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.members.layout.group.GroupIdAndName;
-import 
org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacetForLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacetForLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.BookmarkPolicyFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.CssClassFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.FaFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.ObjectDescribedFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.ObjectNamedFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.tabledec.TableDecoratorFacetForDomainObjectLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.object.promptStyle.PromptStyleFacet;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.CssClassFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.HiddenFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.LabelAtFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MemberDescribedFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MemberNamedFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MultiLineFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.RenderedAdjustedFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.UnchangingFacetForPropertyLayoutXml;
-import 
org.apache.causeway.core.metamodel.layout.LayoutFacetUtil.MetamodelToGridOverridingVisitor;
-import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectMember;
-import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
-
-import static 
org.apache.causeway.core.metamodel.facetapi.FacetUtil.updateFacet;
-import static 
org.apache.causeway.core.metamodel.facetapi.FacetUtil.updateFacetIfPresent;
-
-import lombok.AccessLevel;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@RequiredArgsConstructor(onConstructor_ = {@Inject}, access = 
AccessLevel.PROTECTED)
-@Slf4j
-public abstract class GridSystemServiceAbstract
-implements GridSystemService {
-
-    protected final Provider<SpecificationLoader> specLoaderProvider;
-    protected final TranslationService translationService;
-    protected final MessageService messageService;
-    protected final CausewaySystemEnvironment causewaySystemEnvironment;
-
-    @Override
-    public void normalize(final BSGrid grid, final Class<?> domainClass) {
-
-        // ignore any other grid implementations
-        if(!gridImplementation().isAssignableFrom(grid.getClass())) return;
-
-        final boolean valid = validateAndNormalize(grid, domainClass);
-        if (valid) {
-            overwriteFacets(grid, domainClass);
-            if(log.isDebugEnabled()) {
-                log.debug("Grid:\n\n{}\n\n", toXml(grid));
-            }
-        } else {
-            if(causewaySystemEnvironment.isPrototyping()) {
-                messageService.warnUser("Grid metadata errors for " + 
grid.domainClass().getName() + "; check the error log");
-            }
-            log.error("Grid metadata errors in {}:\n\n{}\n\n", 
grid.domainClass().getName(), toXml(grid));
-        }
-    }
-
-    protected abstract String toXml(BSGrid grid);
-
-    /**
-     * Mandatory hook method for subclasses, where they must ensure that all 
object members (properties, collections
-     * and actions) are in the grid metadata, typically by deriving this 
information from other existing metadata
-     * (eg facets from annotations) or just by applying default rules.
-     */
-    protected abstract boolean validateAndNormalize(
-            final BSGrid grid,
-            final Class<?> domainClass);
-
-    /**
-     * Overwrites (replaces) any existing facets in the metamodel with info 
taken from the grid.
-     *
-     * @implNote This code uses {@link FacetUtil#updateFacet(Facet)}
-     * because the layout might be reloaded from XML if reloading is supported.
-     */
-    private void overwriteFacets(
-            final BSGrid fcGrid,
-            final Class<?> domainClass) {
-
-        var objectSpec = 
specLoaderProvider.get().specForTypeElseFail(domainClass);
-
-        var oneToOneAssociationById = 
ObjectMember.mapById(objectSpec.streamProperties(MixedIn.INCLUDED));
-        var oneToManyAssociationById = 
ObjectMember.mapById(objectSpec.streamCollections(MixedIn.INCLUDED));
-        var objectActionById = 
ObjectMember.mapById(objectSpec.streamRuntimeActions(MixedIn.INCLUDED));
-
-        // governs, whether annotations win over XML grid, based on whether 
XML grid is fallback or 'explicit'
-        var precedence = fcGrid.isFallback()
-                ? Facet.Precedence.LOW // fallback case: XML layout is 
overruled by layout from annotations
-                : Facet.Precedence.HIGH; // non-fallback case: XML layout 
overrules layout from annotations
-
-        final AtomicInteger propertySequence = new AtomicInteger(0);
-        fcGrid.visit(new BSElementVisitor() {
-            private int collectionSequence = 1;
-
-            private int actionDomainObjectSequence = 1;
-            private int actionPropertyGroupSequence = 1;
-            private int actionPropertySequence = 1;
-            private int actionCollectionSequence = 1;
-
-            @Override
-            public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
-
-                updateFacetIfPresent(
-                        BookmarkPolicyFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-                updateFacetIfPresent(
-                        CssClassFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-                updateFacetIfPresent(
-                        FaFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-                updateFacetIfPresent(
-                        ObjectDescribedFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-                updateFacetIfPresent(
-                        ObjectNamedFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-                updateFacetIfPresent(
-                        TableDecoratorFacetForDomainObjectLayoutXml
-                            .create(domainObjectLayoutData, objectSpec, 
precedence));
-            }
-
-            @Override
-            public void visit(final ActionLayoutData actionLayoutData) {
-
-                var actionLayoutDataOwner = actionLayoutData.owner();
-                var objectAction = 
objectActionById.get(actionLayoutData.getId());
-                if(objectAction == null) return;
-
-                {
-                    GroupIdAndName groupIdAndName = null;
-                    int memberOrderSequence;
-                    if(actionLayoutDataOwner instanceof FieldSet) {
-                        var fieldSet = (FieldSet) actionLayoutDataOwner;
-                        for (var propertyLayoutData : 
fieldSet.getProperties()) {
-                            // any will do; choose the first one that we know 
is valid
-                            
if(oneToOneAssociationById.containsKey(propertyLayoutData.getId())) {
-                                groupIdAndName = GroupIdAndName
-                                        
.forPropertyLayoutData(propertyLayoutData)
-                                        .orElse(null);
-                                break;
-                            }
-                        }
-                        memberOrderSequence = actionPropertyGroupSequence++;
-                    } else if(actionLayoutDataOwner instanceof 
PropertyLayoutData) {
-                        groupIdAndName = GroupIdAndName
-                                .forPropertyLayoutData((PropertyLayoutData) 
actionLayoutDataOwner)
-                                .orElse(null);
-                        memberOrderSequence = actionPropertySequence++;
-                    } else if(actionLayoutDataOwner instanceof 
CollectionLayoutData) {
-                        groupIdAndName = GroupIdAndName
-                                
.forCollectionLayoutData((CollectionLayoutData) actionLayoutDataOwner)
-                                .orElse(null);
-                        memberOrderSequence = actionCollectionSequence++;
-                    } else {
-                        // don't add: any existing metadata should be preserved
-                        groupIdAndName = null;
-                        memberOrderSequence = actionDomainObjectSequence++;
-                    }
-                    updateFacet(
-                            
LayoutOrderFacetForLayoutXml.create(memberOrderSequence, objectAction, 
precedence));
-
-                    //XXX hotfix: always override 
LayoutGroupFacetFromActionLayoutAnnotation, otherwise actions are not shown - 
don't know why
-                    var precedenceHotfix = fcGrid.isFallback()
-                            ? Facet.Precedence.DEFAULT
-                            : Facet.Precedence.HIGH;
-
-                    updateFacetIfPresent(
-                            
LayoutGroupFacetForLayoutXml.create(groupIdAndName, objectAction, 
precedenceHotfix));
-                }
-
-                // fix up the action position if required
-                if(actionLayoutDataOwner instanceof FieldSet) {
-                    if(actionLayoutData.getPosition() == null ||
-                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.BELOW ||
-                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.RIGHT) {
-                        
actionLayoutData.setPosition(org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL);
-                    }
-                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) 
{
-                    if(actionLayoutData.getPosition() == null ||
-                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL_DROPDOWN ||
-                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL) {
-                        
actionLayoutData.setPosition(org.apache.causeway.applib.annotation.ActionLayout.Position.BELOW);
-                    }
-                } else {
-                    // doesn't do anything for DomainObject or Collection
-                    actionLayoutData.setPosition(null);
-                }
-
-                updateFacetIfPresent(
-                        
ActionPositionFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
-
-                updateFacetIfPresent(
-                        
CssClassFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
-
-                updateFacetIfPresent(
-                        FaFacetForActionLayoutXml.create(actionLayoutData, 
objectAction, precedence));
-
-                updateFacetIfPresent(
-                        
MemberDescribedFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
-
-                updateFacetIfPresent(
-                        HiddenFacetForActionLayoutXml.create(actionLayoutData, 
objectAction, precedence));
-
-                updateFacetIfPresent(
-                        
MemberNamedFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
-
-                updateFacetIfPresent(
-                    Optional.ofNullable(actionLayoutData)
-                        .map(ActionLayoutData::getPromptStyle)
-                        .map(promptStyle->new 
PromptStyleFacet("ActionLayoutXml", promptStyle, objectAction, precedence, 
true)));
-
-            }
-
-            @Override
-            public void visit(final PropertyLayoutData propertyLayoutData) {
-                var oneToOneAssociation = 
oneToOneAssociationById.get(propertyLayoutData.getId());
-                if(oneToOneAssociation == null) return;
-
-                updateFacetIfPresent(
-                        
CssClassFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
MemberDescribedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
HiddenFacetForPropertyLayoutXml.create(propertyLayoutData, oneToOneAssociation, 
precedence));
-
-                updateFacetIfPresent(
-                        
LabelAtFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
MultiLineFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
MemberNamedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        Optional.ofNullable(propertyLayoutData)
-                            .map(PropertyLayoutData::getPromptStyle)
-                            .map(promptStyle->new 
PromptStyleFacet("PropertyLayoutXml", promptStyle, oneToOneAssociation, 
precedence, true)));
-
-                updateFacetIfPresent(
-                        
RenderedAdjustedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
UnchangingFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        
TypicalLengthFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
-
-                // Layout group-name based on owning property group, Layout 
sequence monotonically increasing
-                // nb for any given field set the sequence won't reset to 
zero; however this is what we want so that
-                // table columns are shown correctly (by fieldset, then 
property order within that fieldset).
-                final FieldSet fieldSet = propertyLayoutData.owner();
-
-                
updateFacet(LayoutOrderFacetForLayoutXml.create(propertySequence.incrementAndGet(),
 oneToOneAssociation, precedence));
-
-                updateFacetIfPresent(
-                        LayoutGroupFacetForLayoutXml.create(fieldSet, 
oneToOneAssociation, precedence));
-            }
-
-            @Override
-            public void visit(final CollectionLayoutData collectionLayoutData) 
{
-                var oneToManyAssociation = 
oneToManyAssociationById.get(collectionLayoutData.getId());
-                if(oneToManyAssociation == null) {
-                    return;
-                }
-
-                updateFacetIfPresent(
-                        CssClassFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        DefaultViewFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        TableDecoratorFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        MemberDescribedFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        HiddenFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        MemberNamedFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        PagedFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacetIfPresent(
-                        SortedByFacetForCollectionLayoutXml
-                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
-
-                updateFacet(LayoutOrderFacetForLayoutXml
-                        .create(collectionSequence++, oneToManyAssociation, 
precedence));
-            }
-        });
-    }
-
-    // --
-
-    @Programmatic
-    @Override
-    public void complete(final BSGrid grid, final Class<?> domainClass) {
-        normalize(grid, domainClass);
-        var objectSpec = 
specLoaderProvider.get().specForTypeElseFail(domainClass);
-        grid.visit(new MetamodelToGridOverridingVisitor(objectSpec));
-    }
-
-    @Programmatic
-    @Override
-    public void minimal(final BSGrid grid, final Class<?> domainClass) {
-        normalize(grid, domainClass);
-        grid.visit(new BSElementVisitor() {
-            @Override
-            public void visit(final ActionLayoutData actionLayoutData) {
-                actionLayoutData.owner().getActions().remove(actionLayoutData);
-            }
-
-            @Override
-            public void visit(final CollectionLayoutData collectionLayoutData) 
{
-                
collectionLayoutData.owner().getCollections().remove(collectionLayoutData);
-            }
-
-            @Override
-            public void visit(final PropertyLayoutData propertyLayoutData) {
-                
propertyLayoutData.owner().getProperties().remove(propertyLayoutData);
-            }
-
-            @Override
-            public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
-                final DomainObjectLayoutDataOwner owner = 
domainObjectLayoutData.owner();
-                owner.setDomainObject(new DomainObjectLayoutData());
-            }
-        });
-    }
-
-}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceBootstrap.java
similarity index 58%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceBootstrap.java
index a63e106344b..c675b83a9c3 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/GridSystemServiceBootstrap.java
@@ -16,14 +16,16 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.bootstrap;
+package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -44,6 +46,7 @@
 import org.apache.causeway.applib.layout.component.ActionLayoutDataOwner;
 import org.apache.causeway.applib.layout.component.CollectionLayoutData;
 import org.apache.causeway.applib.layout.component.DomainObjectLayoutData;
+import org.apache.causeway.applib.layout.component.DomainObjectLayoutDataOwner;
 import org.apache.causeway.applib.layout.component.FieldSet;
 import org.apache.causeway.applib.layout.component.PropertyLayoutData;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSCol;
@@ -52,7 +55,9 @@
 import org.apache.causeway.applib.layout.grid.bootstrap.BSTab;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSTabGroup;
 import org.apache.causeway.applib.layout.grid.bootstrap.Size;
+import 
org.apache.causeway.applib.layout.grid.bootstrap.BSElement.BSElementVisitor;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
+import org.apache.causeway.applib.services.grid.GridSystemService;
 import org.apache.causeway.applib.services.i18n.TranslationService;
 import org.apache.causeway.applib.services.jaxb.JaxbService;
 import org.apache.causeway.applib.services.message.MessageService;
@@ -68,11 +73,45 @@
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.ActionPositionFacetForActionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.CssClassFacetForActionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.FaFacetForActionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.HiddenFacetForActionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.MemberDescribedFacetForActionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.actions.layout.MemberNamedFacetForActionLayoutXml;
 import 
org.apache.causeway.core.metamodel.facets.actions.position.ActionPositionFacet;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.CssClassFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.DefaultViewFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.HiddenFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.MemberDescribedFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.MemberNamedFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.PagedFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.SortedByFacetForCollectionLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.collections.layout.tabledec.TableDecoratorFacetForCollectionLayoutXml;
 import 
org.apache.causeway.core.metamodel.facets.members.layout.group.GroupIdAndName;
 import 
org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacet;
+import 
org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacetForLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacetForLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.BookmarkPolicyFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.CssClassFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.FaFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.ObjectDescribedFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.ObjectNamedFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.domainobjectlayout.tabledec.TableDecoratorFacetForDomainObjectLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.object.promptStyle.PromptStyleFacet;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.CssClassFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.HiddenFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.LabelAtFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MemberDescribedFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MemberNamedFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.MultiLineFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.RenderedAdjustedFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.TypicalLengthFacetForPropertyLayoutXml;
+import 
org.apache.causeway.core.metamodel.facets.properties.propertylayout.UnchangingFacetForPropertyLayoutXml;
 import 
org.apache.causeway.core.metamodel.layout.LayoutFacetUtil.LayoutDataFactory;
-import 
org.apache.causeway.core.metamodel.services.grid.GridSystemServiceAbstract;
+import 
org.apache.causeway.core.metamodel.layout.LayoutFacetUtil.MetamodelToGridOverridingVisitor;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
@@ -82,6 +121,8 @@
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 
 import static org.apache.causeway.commons.internal.base._NullSafe.stream;
+import static 
org.apache.causeway.core.metamodel.facetapi.FacetUtil.updateFacet;
+import static 
org.apache.causeway.core.metamodel.facetapi.FacetUtil.updateFacetIfPresent;
 
 import lombok.Setter;
 import lombok.experimental.Accessors;
@@ -99,7 +140,7 @@
 @Qualifier("Bootstrap")
 @Slf4j
 public class GridSystemServiceBootstrap
-extends GridSystemServiceAbstract {
+implements GridSystemService {
 
     /**
      * SPI to customize layout fallback behavior on a per class basis.
@@ -116,6 +157,9 @@ public static interface FallbackLayoutDataSource {
     @Setter @Accessors(chain = true) // JUnit support
     private GridMarshaller marshaller;
 
+    private final Provider<SpecificationLoader> specLoaderProvider;
+    private final MessageService messageService;
+    private final CausewaySystemEnvironment causewaySystemEnvironment;
     private final CausewayConfiguration config;
     private final Can<FallbackLayoutDataSource> fallbackLayoutDataSources;
 
@@ -128,18 +172,14 @@ public GridSystemServiceBootstrap(
             final MessageService messageService,
             final CausewaySystemEnvironment causewaySystemEnvironment,
             final List<FallbackLayoutDataSource> fallbackLayoutDataSources) {
-        super(specLoaderProvider, translationService, messageService, 
causewaySystemEnvironment);
+        this.specLoaderProvider = specLoaderProvider;
+        this.messageService = messageService;
+        this.causewaySystemEnvironment = causewaySystemEnvironment;
         this.config = causewayConfiguration;
         this.fallbackLayoutDataSources = 
Can.ofCollection(fallbackLayoutDataSources);
     }
 
-    @Override
-    public Class<BSGrid> gridImplementation() {
-        return BSGrid.class;
-    }
-
-    @Override
-    protected String toXml(final BSGrid grid) {
+    private String toXml(final BSGrid grid) {
         return marshaller.marshal(grid, CommonMimeType.XML);
     }
 
@@ -224,8 +264,12 @@ static void addFieldSetsToColumn(
         }
     }
 
-    @Override
-    protected boolean validateAndNormalize(
+    /**
+     * Mandatory hook method for subclasses, where they must ensure that all 
object members (properties, collections
+     * and actions) are in the grid metadata, typically by deriving this 
information from other existing metadata
+     * (eg facets from annotations) or just by applying default rules.
+     */
+    private boolean validateAndNormalize(
             final BSGrid bsGrid,
             final Class<?> domainClass) {
 
@@ -610,4 +654,294 @@ private void addActionTo(
         actionLayoutData.owner(owner);
     }
 
+    @Override
+    public void normalize(final BSGrid grid, final Class<?> domainClass) {
+        final boolean valid = validateAndNormalize(grid, domainClass);
+        if (valid) {
+            overwriteFacets(grid, domainClass);
+            if(log.isDebugEnabled()) {
+                log.debug("Grid:\n\n{}\n\n", toXml(grid));
+            }
+        } else {
+            if(causewaySystemEnvironment.isPrototyping()) {
+                messageService.warnUser("Grid metadata errors for " + 
grid.domainClass().getName() + "; check the error log");
+            }
+            log.error("Grid metadata errors in {}:\n\n{}\n\n", 
grid.domainClass().getName(), toXml(grid));
+        }
+    }
+
+    /**
+     * Overwrites (replaces) any existing facets in the metamodel with info 
taken from the grid.
+     *
+     * @implNote This code uses {@link FacetUtil#updateFacet(Facet)}
+     * because the layout might be reloaded from XML if reloading is supported.
+     */
+    private void overwriteFacets(
+            final BSGrid fcGrid,
+            final Class<?> domainClass) {
+
+        var objectSpec = 
specLoaderProvider.get().specForTypeElseFail(domainClass);
+
+        var oneToOneAssociationById = 
ObjectMember.mapById(objectSpec.streamProperties(MixedIn.INCLUDED));
+        var oneToManyAssociationById = 
ObjectMember.mapById(objectSpec.streamCollections(MixedIn.INCLUDED));
+        var objectActionById = 
ObjectMember.mapById(objectSpec.streamRuntimeActions(MixedIn.INCLUDED));
+
+        // governs, whether annotations win over XML grid, based on whether 
XML grid is fallback or 'explicit'
+        var precedence = fcGrid.isFallback()
+                ? Facet.Precedence.LOW // fallback case: XML layout is 
overruled by layout from annotations
+                : Facet.Precedence.HIGH; // non-fallback case: XML layout 
overrules layout from annotations
+
+        final AtomicInteger propertySequence = new AtomicInteger(0);
+        fcGrid.visit(new BSElementVisitor() {
+            private int collectionSequence = 1;
+
+            private int actionDomainObjectSequence = 1;
+            private int actionPropertyGroupSequence = 1;
+            private int actionPropertySequence = 1;
+            private int actionCollectionSequence = 1;
+
+            @Override
+            public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
+
+                updateFacetIfPresent(
+                        BookmarkPolicyFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+                updateFacetIfPresent(
+                        CssClassFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+                updateFacetIfPresent(
+                        FaFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+                updateFacetIfPresent(
+                        ObjectDescribedFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+                updateFacetIfPresent(
+                        ObjectNamedFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+                updateFacetIfPresent(
+                        TableDecoratorFacetForDomainObjectLayoutXml
+                            .create(domainObjectLayoutData, objectSpec, 
precedence));
+            }
+
+            @Override
+            public void visit(final ActionLayoutData actionLayoutData) {
+
+                var actionLayoutDataOwner = actionLayoutData.owner();
+                var objectAction = 
objectActionById.get(actionLayoutData.getId());
+                if(objectAction == null) return;
+
+                {
+                    GroupIdAndName groupIdAndName = null;
+                    int memberOrderSequence;
+                    if(actionLayoutDataOwner instanceof FieldSet) {
+                        var fieldSet = (FieldSet) actionLayoutDataOwner;
+                        for (var propertyLayoutData : 
fieldSet.getProperties()) {
+                            // any will do; choose the first one that we know 
is valid
+                            
if(oneToOneAssociationById.containsKey(propertyLayoutData.getId())) {
+                                groupIdAndName = GroupIdAndName
+                                        
.forPropertyLayoutData(propertyLayoutData)
+                                        .orElse(null);
+                                break;
+                            }
+                        }
+                        memberOrderSequence = actionPropertyGroupSequence++;
+                    } else if(actionLayoutDataOwner instanceof 
PropertyLayoutData) {
+                        groupIdAndName = GroupIdAndName
+                                .forPropertyLayoutData((PropertyLayoutData) 
actionLayoutDataOwner)
+                                .orElse(null);
+                        memberOrderSequence = actionPropertySequence++;
+                    } else if(actionLayoutDataOwner instanceof 
CollectionLayoutData) {
+                        groupIdAndName = GroupIdAndName
+                                
.forCollectionLayoutData((CollectionLayoutData) actionLayoutDataOwner)
+                                .orElse(null);
+                        memberOrderSequence = actionCollectionSequence++;
+                    } else {
+                        // don't add: any existing metadata should be preserved
+                        groupIdAndName = null;
+                        memberOrderSequence = actionDomainObjectSequence++;
+                    }
+                    updateFacet(
+                            
LayoutOrderFacetForLayoutXml.create(memberOrderSequence, objectAction, 
precedence));
+
+                    //XXX hotfix: always override 
LayoutGroupFacetFromActionLayoutAnnotation, otherwise actions are not shown - 
don't know why
+                    var precedenceHotfix = fcGrid.isFallback()
+                            ? Facet.Precedence.DEFAULT
+                            : Facet.Precedence.HIGH;
+
+                    updateFacetIfPresent(
+                            
LayoutGroupFacetForLayoutXml.create(groupIdAndName, objectAction, 
precedenceHotfix));
+                }
+
+                // fix up the action position if required
+                if(actionLayoutDataOwner instanceof FieldSet) {
+                    if(actionLayoutData.getPosition() == null ||
+                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.BELOW ||
+                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.RIGHT) {
+                        
actionLayoutData.setPosition(org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL);
+                    }
+                } else if(actionLayoutDataOwner instanceof PropertyLayoutData) 
{
+                    if(actionLayoutData.getPosition() == null ||
+                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL_DROPDOWN ||
+                            actionLayoutData.getPosition() == 
org.apache.causeway.applib.annotation.ActionLayout.Position.PANEL) {
+                        
actionLayoutData.setPosition(org.apache.causeway.applib.annotation.ActionLayout.Position.BELOW);
+                    }
+                } else {
+                    // doesn't do anything for DomainObject or Collection
+                    actionLayoutData.setPosition(null);
+                }
+
+                updateFacetIfPresent(
+                        
ActionPositionFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
+
+                updateFacetIfPresent(
+                        
CssClassFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
+
+                updateFacetIfPresent(
+                        FaFacetForActionLayoutXml.create(actionLayoutData, 
objectAction, precedence));
+
+                updateFacetIfPresent(
+                        
MemberDescribedFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
+
+                updateFacetIfPresent(
+                        HiddenFacetForActionLayoutXml.create(actionLayoutData, 
objectAction, precedence));
+
+                updateFacetIfPresent(
+                        
MemberNamedFacetForActionLayoutXml.create(actionLayoutData, objectAction, 
precedence));
+
+                updateFacetIfPresent(
+                    Optional.ofNullable(actionLayoutData)
+                        .map(ActionLayoutData::getPromptStyle)
+                        .map(promptStyle->new 
PromptStyleFacet("ActionLayoutXml", promptStyle, objectAction, precedence, 
true)));
+
+            }
+
+            @Override
+            public void visit(final PropertyLayoutData propertyLayoutData) {
+                var oneToOneAssociation = 
oneToOneAssociationById.get(propertyLayoutData.getId());
+                if(oneToOneAssociation == null) return;
+
+                updateFacetIfPresent(
+                        
CssClassFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
MemberDescribedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
HiddenFacetForPropertyLayoutXml.create(propertyLayoutData, oneToOneAssociation, 
precedence));
+
+                updateFacetIfPresent(
+                        
LabelAtFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
MultiLineFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
MemberNamedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        Optional.ofNullable(propertyLayoutData)
+                            .map(PropertyLayoutData::getPromptStyle)
+                            .map(promptStyle->new 
PromptStyleFacet("PropertyLayoutXml", promptStyle, oneToOneAssociation, 
precedence, true)));
+
+                updateFacetIfPresent(
+                        
RenderedAdjustedFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
UnchangingFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        
TypicalLengthFacetForPropertyLayoutXml.create(propertyLayoutData, 
oneToOneAssociation, precedence));
+
+                // Layout group-name based on owning property group, Layout 
sequence monotonically increasing
+                // nb for any given field set the sequence won't reset to 
zero; however this is what we want so that
+                // table columns are shown correctly (by fieldset, then 
property order within that fieldset).
+                final FieldSet fieldSet = propertyLayoutData.owner();
+
+                
updateFacet(LayoutOrderFacetForLayoutXml.create(propertySequence.incrementAndGet(),
 oneToOneAssociation, precedence));
+
+                updateFacetIfPresent(
+                        LayoutGroupFacetForLayoutXml.create(fieldSet, 
oneToOneAssociation, precedence));
+            }
+
+            @Override
+            public void visit(final CollectionLayoutData collectionLayoutData) 
{
+                var oneToManyAssociation = 
oneToManyAssociationById.get(collectionLayoutData.getId());
+                if(oneToManyAssociation == null) {
+                    return;
+                }
+
+                updateFacetIfPresent(
+                        CssClassFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        DefaultViewFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        TableDecoratorFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        MemberDescribedFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        HiddenFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        MemberNamedFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        PagedFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacetIfPresent(
+                        SortedByFacetForCollectionLayoutXml
+                            .create(collectionLayoutData, 
oneToManyAssociation, precedence));
+
+                updateFacet(LayoutOrderFacetForLayoutXml
+                        .create(collectionSequence++, oneToManyAssociation, 
precedence));
+            }
+        });
+    }
+
+    // --
+
+    @Override
+    public void complete(final BSGrid grid, final Class<?> domainClass) {
+        normalize(grid, domainClass);
+        var objectSpec = 
specLoaderProvider.get().specForTypeElseFail(domainClass);
+        grid.visit(new MetamodelToGridOverridingVisitor(objectSpec));
+    }
+
+    @Override
+    public void minimal(final BSGrid grid, final Class<?> domainClass) {
+        normalize(grid, domainClass);
+        grid.visit(new BSElementVisitor() {
+            @Override
+            public void visit(final ActionLayoutData actionLayoutData) {
+                actionLayoutData.owner().getActions().remove(actionLayoutData);
+            }
+
+            @Override
+            public void visit(final CollectionLayoutData collectionLayoutData) 
{
+                
collectionLayoutData.owner().getCollections().remove(collectionLayoutData);
+            }
+
+            @Override
+            public void visit(final PropertyLayoutData propertyLayoutData) {
+                
propertyLayoutData.owner().getProperties().remove(propertyLayoutData);
+            }
+
+            @Override
+            public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
+                final DomainObjectLayoutDataOwner owner = 
domainObjectLayoutData.owner();
+                owner.setDomainObject(new DomainObjectLayoutData());
+            }
+        });
+    }
+
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/XsiSchemaLocationProviderForGrid.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/XsiSchemaLocationProviderForGrid.java
index fe18cc31c36..be3b4352239 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/XsiSchemaLocationProviderForGrid.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/XsiSchemaLocationProviderForGrid.java
@@ -23,7 +23,7 @@
 
 import jakarta.xml.bind.Marshaller;
 
-public record XsiSchemaLocationProviderForGrid() {
+record XsiSchemaLocationProviderForGrid() {
 
     static final String COMPONENT_TNS = 
"https://causeway.apache.org/applib/layout/component";;
     static final String COMPONENT_SCHEMA_LOCATION = 
"https://causeway.apache.org/applib/layout/component/component.xsd";;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_UnreferencedSequenceUtil.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/_UnreferencedSequenceUtil.java
similarity index 97%
rename from 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_UnreferencedSequenceUtil.java
rename to 
core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/_UnreferencedSequenceUtil.java
index 937771e64da..90d8c816ded 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_UnreferencedSequenceUtil.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/_UnreferencedSequenceUtil.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.core.metamodel.services.grid.bootstrap;
+package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.List;
 import java.util.stream.Collectors;
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
index 26d0d33c103..82c8aff25b7 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResource.java
@@ -20,11 +20,11 @@
 
 import org.jspecify.annotations.NonNull;
 
-import org.apache.causeway.applib.value.NamedWithMimeType;
+import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
 
 public record LayoutResource(
         @NonNull String resourceName,
-        NamedWithMimeType.@NonNull CommonMimeType format,
+        @NonNull CommonMimeType format,
         @NonNull String content) {
 
 }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
index a3edd90dee1..7e280f337ba 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoader.java
@@ -20,14 +20,12 @@
 
 import java.util.Optional;
 
-import org.apache.causeway.applib.annotation.Programmatic;
-import org.apache.causeway.commons.functional.Try;
-import 
org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault;
-
 import org.jspecify.annotations.NonNull;
 
+import org.apache.causeway.commons.functional.Try;
+
 /**
- * A simpler SPI for {@link GridLoaderServiceDefault}.
+ * SPI for grid loading.
  *
  * @since 2.0 {@index}
  */
@@ -36,7 +34,6 @@ public interface LayoutResourceLoader {
     /**
      * Try to locate and load a {@link LayoutResource} by type and name.
      */
-    @Programmatic
     Try<LayoutResource> tryLoadLayoutResource(
             final @NonNull Class<?> type,
             final @NonNull String candidateResourceName);
@@ -45,10 +42,9 @@ Try<LayoutResource> tryLoadLayoutResource(
      * Optionally returns a {@link LayoutResource} based
      * on whether it could be resolved by type and name
      * and successfully read.
-     * <p>
-     * Silently ignores exceptions underneath, if any.
+     *
+     * <p>Silently ignores exceptions underneath, if any.
      */
-    @Programmatic
     default Optional<LayoutResource> lookupLayoutResource(
             final @NonNull Class<?> type,
             final @NonNull String candidateResourceName) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
index 606d77367f9..ea5e29c4689 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/spi/LayoutResourceLoaderDefault.java
@@ -21,6 +21,8 @@
 import jakarta.annotation.Priority;
 import jakarta.inject.Named;
 
+import org.jspecify.annotations.NonNull;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -29,8 +31,6 @@
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.io.DataSource;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
-import org.jspecify.annotations.NonNull;
-import lombok.RequiredArgsConstructor;
 
 /**
  * Default implementation of {@link LayoutResourceLoader}.
@@ -41,9 +41,8 @@
 @Named(CausewayModuleCoreMetamodel.NAMESPACE + ".LayoutResourceLoaderDefault")
 @Priority(PriorityPrecedence.MIDPOINT)
 @Qualifier("Default")
-@RequiredArgsConstructor //JUnit Support
 //@Slf4j
-public class LayoutResourceLoaderDefault implements LayoutResourceLoader {
+public record LayoutResourceLoaderDefault() implements LayoutResourceLoader {
 
     @Override
     public Try<LayoutResource> tryLoadLayoutResource(
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault_resourceNameTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
similarity index 72%
rename from 
core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault_resourceNameTest.java
rename to 
core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
index 97ef3caa513..dc64a762030 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoaderServiceDefault_resourceNameTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridCache_resourceNameTest.java
@@ -19,6 +19,7 @@
 package org.apache.causeway.core.metamodel.services.grid;
 
 import java.util.EnumSet;
+import java.util.List;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -26,57 +27,56 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
-import org.apache.causeway.commons.collections.Can;
-import 
org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault.LayoutKey;
+import org.apache.causeway.core.metamodel.services.grid.GridLoader.LayoutKey;
 import org.apache.causeway.core.metamodel.services.grid.spi.LayoutResource;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoader;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
 
-class GridLoaderServiceDefault_resourceNameTest {
+class GridCache_resourceNameTest {
 
-    private GridLoaderServiceDefault gridLoaderServiceDefault;
+    private GridCache gridCache;
     private LayoutResourceLoader layoutResourceLoader;
 
     @BeforeEach
     void setUp() throws Exception {
         layoutResourceLoader = new LayoutResourceLoaderDefault();
-        gridLoaderServiceDefault = new GridLoaderServiceDefault(null, 
Can.of(layoutResourceLoader), false);
+        gridCache = new GridCache(null, false, List.of(layoutResourceLoader));
     }
 
     @Test
     void when_default_exists() {
         assertEquals(
                 "Foo.layout.xml",
-                resourceNameFor(new 
GridLoaderServiceDefault.LayoutKey(Foo.class, null)));
+                resourceNameFor(new GridLoader.LayoutKey(Foo.class, null)));
     }
 
     @Test
     void when_fallback_exists() {
         assertEquals(
                 "Foo2.layout.fallback.xml",
-                resourceNameFor(new 
GridLoaderServiceDefault.LayoutKey(Foo2.class, null)));
+                resourceNameFor(new GridLoader.LayoutKey(Foo2.class, null)));
     }
 
     @Test
     void when_default_and_fallback_both_exist() {
         assertEquals(
                 "Foo3.layout.xml",
-                resourceNameFor(new 
GridLoaderServiceDefault.LayoutKey(Foo3.class, null)));
+                resourceNameFor(new GridLoader.LayoutKey(Foo3.class, null)));
     }
 
     @Test
     void when_neither_exist() {
         assertEquals(
                 (String)null,
-                resourceNameFor(new 
GridLoaderServiceDefault.LayoutKey(Foo4.class, null)));
+                resourceNameFor(new GridLoader.LayoutKey(Foo4.class, null)));
     }
 
     // -- HELPER
 
     private String resourceNameFor(final LayoutKey dcal) {
-        return gridLoaderServiceDefault.loadLayoutResource(dcal, 
EnumSet.of(CommonMimeType.XML))
-        .map(LayoutResource::resourceName)
-        .orElse(null);
+        return gridCache.gridLoader().loadLayoutResource(dcal, 
EnumSet.of(CommonMimeType.XML))
+            .map(LayoutResource::resourceName)
+            .orElse(null);
     }
 
 }
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
index fbf06ae92fe..aeda1067fa7 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/GridLoadingTest.java
@@ -26,34 +26,49 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import org.apache.causeway.applib.services.grid.GridLoaderService;
+import org.apache.causeway.applib.services.grid.GridService;
 import org.apache.causeway.applib.services.layout.LayoutExportStyle;
 import org.apache.causeway.applib.services.layout.LayoutService;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
+import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
 import org.apache.causeway.core.metamodel.MetaModelTestAbstract;
 import org.apache.causeway.core.metamodel.facetapi.Facet.Precedence;
 import org.apache.causeway.core.metamodel.facets.all.named.MemberNamedFacet;
 import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
+import 
org.apache.causeway.core.mmtestsupport.MetaModelContext_forTesting.MetaModelContext_forTestingBuilder;
 
 class GridLoadingTest
 extends MetaModelTestAbstract {
 
-    private GridLoaderServiceDefault gridLoaderService;
+    private GridCache gridCache;
     private LayoutService layoutService;
 
+    @Override
+    protected void onSetUp(final MetaModelContext_forTestingBuilder 
mmcBuilder) {
+        CausewaySystemEnvironment.setPrototyping(true);
+        var env = new CausewaySystemEnvironment();
+        CausewaySystemEnvironment.setPrototyping(false);
+        assertTrue(env.isPrototyping());
+        mmcBuilder.systemEnvironment(env);
+        super.onSetUp(mmcBuilder);
+    }
+
     @Override
     protected void afterSetUp() {
         layoutService = 
getServiceRegistry().lookupServiceElseFail(LayoutService.class);
-        gridLoaderService = (GridLoaderServiceDefault)getServiceRegistry()
-                .lookupServiceElseFail(GridLoaderService.class);
+        gridCache = ((GridServiceDefault) getServiceRegistry()
+                .lookupServiceElseFail(GridService.class))
+                .gridCache();
+        assertTrue(gridCache.supportsReloading());
     }
 
     // test blueprint, for future work
     void blueprint() {
-        var domainClassAndLayout = new 
GridLoaderServiceDefault.LayoutKey(Bar.class, null);
-        gridLoaderService.loadLayoutResource(domainClassAndLayout, 
EnumSet.of(CommonMimeType.XML));
+        var domainClassAndLayout = new GridLoader.LayoutKey(Bar.class, null);
+        gridCache.gridLoader().loadLayoutResource(domainClassAndLayout, 
EnumSet.of(CommonMimeType.XML));
 
         var xml = layoutService.objectLayout(Bar.class, 
LayoutExportStyle.MINIMAL, CommonMimeType.XML);
         System.out.println(xml);
diff --git 
a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
 
b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
index 71af4f7c29d..ed22ea59e17 100644
--- 
a/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
+++ 
b/core/mmtestsupport/src/main/java/org/apache/causeway/core/mmtestsupport/MetaModelContext_forTesting.java
@@ -35,7 +35,6 @@
 import org.springframework.util.ClassUtils;
 
 import org.apache.causeway.applib.services.factory.FactoryService;
-import org.apache.causeway.applib.services.grid.GridLoaderService;
 import org.apache.causeway.applib.services.grid.GridMarshaller;
 import org.apache.causeway.applib.services.grid.GridService;
 import org.apache.causeway.applib.services.grid.GridSystemService;
@@ -90,11 +89,9 @@
 import 
org.apache.causeway.core.metamodel.services.classsubstitutor.ClassSubstitutorRegistry;
 import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory;
 import 
org.apache.causeway.core.metamodel.services.events.MetamodelEventService;
-import 
org.apache.causeway.core.metamodel.services.grid.GridLoaderServiceDefault;
+import org.apache.causeway.core.metamodel.services.grid.GridMarshallerXml;
 import org.apache.causeway.core.metamodel.services.grid.GridServiceDefault;
-import 
org.apache.causeway.core.metamodel.services.grid.XsiSchemaLocationProviderForGrid;
-import 
org.apache.causeway.core.metamodel.services.grid.bootstrap.GridMarshallerServiceBootstrap;
-import 
org.apache.causeway.core.metamodel.services.grid.bootstrap.GridSystemServiceBootstrap;
+import 
org.apache.causeway.core.metamodel.services.grid.GridSystemServiceBootstrap;
 import 
org.apache.causeway.core.metamodel.services.grid.spi.LayoutResourceLoaderDefault;
 import org.apache.causeway.core.metamodel.services.layout.LayoutServiceDefault;
 import org.apache.causeway.core.metamodel.services.message.MessageServiceNoop;
@@ -300,7 +297,6 @@ Stream<SingletonBeanProvider> streamBeanAdapters() {
                 discoveredServices.stream(),
                 Stream.of(
                     // support for lazy bean providers,
-                    
SingletonBeanProvider.forTestingLazy(GridLoaderService.class, 
this::getGridLoaderService),
                     SingletonBeanProvider.forTestingLazy(GridService.class, 
this::getGridService),
                     SingletonBeanProvider.forTestingLazy(JaxbService.class, 
this::getJaxbService),
                     
SingletonBeanProvider.forTestingLazy(MenuBarsService.class, 
this::getMenuBarsService),
@@ -458,7 +454,7 @@ private final MenuBarsService createMenuBarsService() {
     @Getter(lazy = true)
     private final GridMarshaller gridMarshaller = createGridMarshaller();
     private final GridMarshaller createGridMarshaller() {
-        return new GridMarshallerServiceBootstrap(getJaxbService(), new 
XsiSchemaLocationProviderForGrid());
+        return new GridMarshallerXml(getJaxbService());
     }
 
     @Getter(lazy = true)
@@ -476,19 +472,15 @@ private final List<GridSystemService> 
createGridSystemService() {
                 .setMarshaller(getGridMarshaller()));
     }
 
-    @Getter(lazy = true)
-    private final GridLoaderService gridLoaderService = 
createGridLoaderService();
-    private final GridLoaderService createGridLoaderService() {
-        return new GridLoaderServiceDefault(getMessageService(), Can.of(new 
LayoutResourceLoaderDefault()), /*support reloading*/true);
-    }
-
     @Getter(lazy = true)
     private final GridService gridService = createGridService();
     private final GridService createGridService() {
         return new GridServiceDefault(
-            getGridLoaderService(),
+            getSystemEnvironment(),
             getGridMarshaller(),
-            getGridSystemServices());
+            getMessageService(),
+            getGridSystemServices(),
+            List.of(new LayoutResourceLoaderDefault()));
     }
 
     @Getter(lazy = true)

Reply via email to