diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java
new file mode 100644
index 00000000..236c3a49
--- /dev/null
+++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.gemfire.support;
+
+import java.util.Arrays;
+import java.util.function.Supplier;
+
+import org.apache.geode.cache.CacheClosedException;
+import org.apache.geode.cache.GemFireCache;
+
+import org.springframework.data.gemfire.CacheResolver;
+import org.springframework.data.gemfire.util.ArrayUtils;
+import org.springframework.data.gemfire.util.CollectionUtils;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Composition of {@link CacheResolver CacheResolvers} using
+ * the Composite Software Design Pattern that acts,
+ * and can be referred to, as a single instance of {@link CacheResolver}.
+ *
+ * This implementation also supports caching the result of the resolution of the {@link GemFireCache}
+ * instance reference.
+ *
+ * @author John Blum
+ * @see org.apache.geode.cache.GemFireCache
+ * @see org.springframework.data.gemfire.CacheResolver
+ * @see org.springframework.data.gemfire.support.AbstractCachingCacheResolver
+ * @since 2.3.0
+ */
+public class ComposableCacheResolver extends AbstractCachingCacheResolver {
+
+ private final CacheResolver cacheResolverOne;
+ private final CacheResolver cacheResolverTwo;
+
+ /**
+ * Null-safe factory method to compose an array of {@link CacheResolver} objects in a composition.
+ *
+ * @param {@link Class subclass} of {@link GemFireCache}.
+ * @param cacheResolvers array of {@link CacheResolver CacheResolvers} to compose; may be {@literal null}.
+ * @return a composition from the array of {@link CacheResolver CacheResolvers}; may be {@literal null}.
+ * @see org.springframework.data.gemfire.CacheResolver
+ * @see #compose(Iterable)
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public static CacheResolver compose(@Nullable CacheResolver... cacheResolvers) {
+ return compose(Arrays.asList(ArrayUtils.nullSafeArray(cacheResolvers, CacheResolver.class)));
+ }
+
+ /**
+ * Null-safe factory method to compose an {@link Iterable} of {@link CacheResolver} objects in a composition.
+ *
+ * @param {@link Class subclass} of {@link GemFireCache}.
+ * @param cacheResolvers {@link Iterable} of {@link CacheResolver CacheResolvers} to compose; may be {@literal null}.
+ * @return a composition from the {@link Iterable} of {@link CacheResolver CacheResolvers}; may be {@literal null}.
+ * @see org.springframework.data.gemfire.CacheResolver
+ * @see java.lang.Iterable
+ * @see #compose(CacheResolver, CacheResolver)
+ */
+ @Nullable
+ public static CacheResolver compose(@Nullable Iterable> cacheResolvers) {
+
+ CacheResolver current = null;
+
+ for (CacheResolver cacheResolver : CollectionUtils.nullSafeIterable(cacheResolvers)) {
+ current = compose(current, cacheResolver);
+ }
+
+ return current;
+ }
+
+ /**
+ * Null-safe factory method used to compose two {@link CacheResolver} objects in a composition.
+ *
+ * @param {@link Class subclass} of {@link GemFireCache}.
+ * @param one first {@link CacheResolver} in the composition.
+ * @param two second {@link CacheResolver} in the composition.
+ * @return the first {@link CacheResolver} if the second {@link CacheResolver} is {@literal null}.
+ * Return the second {@link CacheResolver} if the first {@link CacheResolver} is {@literal null}.
+ * Otherwise, return a composition of both {@link CacheResolver} one and two as a {@link ComposableCacheResolver}.
+ * @see org.springframework.data.gemfire.CacheResolver
+ * @see #ComposableCacheResolver(CacheResolver, CacheResolver)
+ */
+ @Nullable
+ public static CacheResolver compose(@Nullable CacheResolver one,
+ @Nullable CacheResolver two) {
+
+ return one == null ? two : two == null ? one : new ComposableCacheResolver<>(one, two);
+ }
+
+ /**
+ * Constructs a new instance of {@link ComposableCacheResolver} initialized and composed with
+ * the given {@link CacheResolver} objects/arguments.
+ *
+ * @param cacheResolverOne first {@link CacheResolver} in the composition.
+ * @param cacheResolverTwo second {@link CacheResolver} in the composition.
+ * @throws IllegalArgumentException if either {@link CacheResolver} argument is {@literal null}.
+ * @see org.springframework.data.gemfire.CacheResolver
+ */
+ protected ComposableCacheResolver(CacheResolver cacheResolverOne, CacheResolver cacheResolverTwo) {
+
+ Assert.notNull(cacheResolverOne, "CacheResolver 1 must not be null");
+ Assert.notNull(cacheResolverTwo, "CacheResolver 2 must not be null");
+
+ this.cacheResolverOne = cacheResolverOne;
+ this.cacheResolverTwo = cacheResolverTwo;
+ }
+
+ /**
+ * Returns a reference to the first, non-null, configured {@link CacheResolver} in the composition.
+ *
+ * @return a reference to the first {@link CacheResolver} in the composition.
+ * @see org.springframework.data.gemfire.CacheResolver
+ */
+ protected @NonNull CacheResolver getCacheResolverOne() {
+ return this.cacheResolverOne;
+ }
+
+ /**
+ * Returns a reference to the second, non-null, configured {@link CacheResolver} in the composition.
+ *
+ * @return a reference to the second {@link CacheResolver} in the composition.
+ * @see org.springframework.data.gemfire.CacheResolver
+ */
+ protected CacheResolver getCacheResolverTwo() {
+ return this.cacheResolverTwo;
+ }
+
+ /**
+ * Resolves the first, single reference to a {@link GemFireCache}, handling any {@link Exception Exceptions}
+ * throwing by composed {@link CacheResolver CacheResolvers}, such as a {@link CacheClosedException}.
+ *
+ * This method may ultimately still result in a thrown {@link Exception}, but it will make a best effort to
+ * exhaustively consult all composed {@link CacheResolver CacheResolvers}.
+ *
+ * @return the first, single resolved reference to a {@link GemFireCache}.
+ * @see org.springframework.data.gemfire.CacheResolver#resolve()
+ * @see org.apache.geode.cache.GemFireCache
+ * @see #getCacheResolverOne()
+ * @see #getCacheResolverTwo()
+ */
+ @Override
+ public T doResolve() {
+
+ Supplier cacheSupplier = () -> getCacheResolverTwo().resolve();
+
+ try {
+
+ T cache = getCacheResolverOne().resolve();
+
+ return cache != null ? cache : cacheSupplier.get();
+ }
+ catch (Throwable ignore) {
+ return cacheSupplier.get();
+ }
+ }
+}
diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java
new file mode 100644
index 00000000..6fd7938a
--- /dev/null
+++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.gemfire.support;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.function.Supplier;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import org.apache.geode.cache.CacheClosedException;
+import org.apache.geode.cache.GemFireCache;
+
+import org.springframework.data.gemfire.CacheResolver;
+
+/**
+ * Unit Tests for {@link ComposableCacheResolver}.
+ *
+ * @author John Blum
+ * @see org.junit.Test
+ * @see org.mockito.Mock
+ * @see org.mockito.Mockito
+ * @see org.mockito.junit.MockitoJUnitRunner
+ * @see org.apache.geode.cache.GemFireCache
+ * @see org.springframework.data.gemfire.support.ComposableCacheResolver
+ * @since 2.3.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class ComposableCacheResolverUnitTests {
+
+ @Mock
+ private GemFireCache mockCache;
+
+ @Mock(name = "CacheResolverOne")
+ private CacheResolver mockCacheResolverOne;
+
+ @Mock(name = "CacheResolverTwo")
+ private CacheResolver mockCacheResolverTwo;
+
+ @Mock(name = "CacheResolverThree")
+ private CacheResolver mockCacheResolverThree;
+
+ @Test
+ public void composeAllNullCacheResolvers() {
+ assertThat(ComposableCacheResolver.compose(null, null)).isNull();
+ }
+
+ @Test
+ public void composeOneNullCacheResolver() {
+
+ assertThat(ComposableCacheResolver.compose(this.mockCacheResolverOne, null))
+ .isSameAs(this.mockCacheResolverOne);
+
+ assertThat(ComposableCacheResolver.compose(null, this.mockCacheResolverTwo))
+ .isSameAs(this.mockCacheResolverTwo);
+ }
+
+ @Test
+ public void composeTwoNonNullCacheResolvers() {
+
+ CacheResolver composed = ComposableCacheResolver.compose(this.mockCacheResolverOne, this.mockCacheResolverTwo);
+
+ assertThat(composed).isInstanceOf(ComposableCacheResolver.class);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo);
+ }
+
+ @Test
+ @SuppressWarnings("all")
+ public void composeArrayOfCacheResolversBuildsComposition() {
+
+ CacheResolver[] cacheResolvers = {
+ this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree
+ };
+
+ CacheResolver composed = ComposableCacheResolver.compose(cacheResolvers);
+
+ assertThat(composed).isInstanceOf(ComposableCacheResolver.class);
+
+ CacheResolver cacheResolverOne = ((ComposableCacheResolver) composed).getCacheResolverOne();
+ CacheResolver cacheResolverTwo = ((ComposableCacheResolver) composed).getCacheResolverTwo();
+
+ assertThat(cacheResolverOne).isInstanceOf(ComposableCacheResolver.class);
+ assertThat(((ComposableCacheResolver) cacheResolverOne).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne);
+ assertThat(((ComposableCacheResolver) cacheResolverOne).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo);
+ assertThat(cacheResolverTwo).isEqualTo(this.mockCacheResolverThree);
+ }
+
+ @Test
+ @SuppressWarnings("all")
+ public void composeArrayContainingCombinationOfNonNullAndNullCacheResolverReturnsNonNullCacheResolver() {
+
+ CacheResolver[] cacheResolvers = { null, this.mockCacheResolverTwo, null, null };
+
+ assertThat(ComposableCacheResolver.compose(cacheResolvers)).isSameAs(this.mockCacheResolverTwo);
+
+ cacheResolvers = new CacheResolver[] { null, this.mockCacheResolverTwo, this.mockCacheResolverThree, null };
+
+ CacheResolver composed = ComposableCacheResolver.compose(cacheResolvers);
+
+ assertThat(composed).isInstanceOf(ComposableCacheResolver.class);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverTwo);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverThree);
+
+ cacheResolvers = new CacheResolver[] { null, this.mockCacheResolverOne, null, null, this.mockCacheResolverTwo };
+
+ composed = ComposableCacheResolver.compose(cacheResolvers);
+
+ assertThat(composed).isInstanceOf(ComposableCacheResolver.class);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne);
+ assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo);
+ }
+
+ @Test
+ public void composeArrayContainingNonNullCacheResolverAndNullCacheResolverReturnsNonNullCacheResolver() {
+
+ CacheResolver[] cacheResolvers = { null, this.mockCacheResolverTwo };
+
+ assertThat(ComposableCacheResolver.compose(cacheResolvers)).isSameAs(this.mockCacheResolverTwo);
+ }
+
+ @Test
+ public void composeArrayContainingSingleCacheResolverReturnsSingleCacheResolver() {
+ assertThat(ComposableCacheResolver.compose(this.mockCacheResolverOne)).isSameAs(this.mockCacheResolverOne);
+ }
+
+ @Test
+ public void composeWithEmptyArrayReturnsNull() {
+ assertThat(ComposableCacheResolver.compose()).isNull();
+ }
+
+ @Test
+ public void composeWithNullArrayReturnsNull() {
+ assertThat(ComposableCacheResolver.compose((CacheResolver[]) null)).isNull();
+ }
+
+ @Test
+ public void composeWithNulIterableReturnsNull() {
+ assertThat(ComposableCacheResolver.compose((Iterable) null)).isNull();
+ }
+
+ @Test
+ public void constructComposableCacheResolver() {
+
+ ComposableCacheResolver cacheResolver =
+ new ComposableCacheResolver(this.mockCacheResolverOne, this.mockCacheResolverTwo);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne);
+ assertThat(cacheResolver.getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo);
+ }
+
+ public void testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException(
+ Supplier cacheResolverSupplier, String expectedMessage) {
+
+ try {
+ cacheResolverSupplier.get();
+ }
+ catch (IllegalArgumentException expected) {
+
+ assertThat(expected).hasMessage(expectedMessage);
+ assertThat(expected).hasNoCause();
+
+ throw expected;
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructComposableCacheResolverWithNullCacheResolverAsFirstArgument() {
+
+ testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException(
+ () -> new ComposableCacheResolver(null, this.mockCacheResolverTwo),
+ "CacheResolver 1 must not be null");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructComposableCacheResolverWithNullCacheResolverAsSecondArgument() {
+
+ testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException(
+ () -> new ComposableCacheResolver(this.mockCacheResolverOne, null),
+ "CacheResolver 2 must not be null");
+ }
+
+ @Test
+ public void resolvesGemFireCacheFromLastCacheResolver() {
+
+ when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache);
+
+ CacheResolver cacheResolver = ComposableCacheResolver
+ .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache);
+
+ verify(this.mockCacheResolverOne, times(1)).resolve();
+ verify(this.mockCacheResolverTwo, times(1)).resolve();
+ verify(this.mockCacheResolverThree, times(1)).resolve();
+ }
+
+ @Test
+ public void resolvesGemFireCacheFromLastCacheResolverWithExceptionThrowingCacheResolvers() {
+
+ when(this.mockCacheResolverOne.resolve()).thenThrow(new CacheClosedException("ONE"));
+ when(this.mockCacheResolverTwo.resolve()).thenThrow(new CacheClosedException("ONE"));
+ when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache);
+
+ CacheResolver cacheResolver = ComposableCacheResolver
+ .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache);
+
+ verify(this.mockCacheResolverOne, times(1)).resolve();
+ verify(this.mockCacheResolverTwo, times(1)).resolve();
+ verify(this.mockCacheResolverThree, times(1)).resolve();
+ }
+
+ @Test
+ public void resolvesGemFireCacheFromLastCacheResolverWithNullReturningAndExceptionThrowingCacheResolvers() {
+
+ when(this.mockCacheResolverOne.resolve()).thenReturn(null);
+ when(this.mockCacheResolverTwo.resolve()).thenThrow(new CacheClosedException("ONE"));
+ when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache);
+
+ CacheResolver cacheResolver = ComposableCacheResolver
+ .compose(this.mockCacheResolverOne, null, this.mockCacheResolverTwo, null,
+ this.mockCacheResolverThree);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache);
+
+ verify(this.mockCacheResolverOne, times(1)).resolve();
+ verify(this.mockCacheResolverTwo, times(2)).resolve();
+ verify(this.mockCacheResolverThree, times(1)).resolve();
+ }
+
+ @Test
+ public void shortCircuitsAndResolvesGemFireCacheFromFirstCacheResolver() {
+
+ when(this.mockCacheResolverOne.resolve()).thenReturn(this.mockCache);
+
+ CacheResolver cacheResolver = ComposableCacheResolver
+ .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache);
+
+ verify(this.mockCacheResolverOne, times(1)).resolve();
+ verify(this.mockCacheResolverTwo, never()).resolve();
+ verify(this.mockCacheResolverThree, never()).resolve();
+ }
+
+ @Test
+ public void shortCircuitsAndResolvesGemFireCacheFromSecondCacheResolver() {
+
+ when(this.mockCacheResolverOne.resolve()).thenThrow(new CacheClosedException("ONE"));
+ when(this.mockCacheResolverTwo.resolve()).thenReturn(this.mockCache);
+
+ CacheResolver cacheResolver = ComposableCacheResolver
+ .compose(null, this.mockCacheResolverOne, this.mockCacheResolverTwo, null, null,
+ this.mockCacheResolverThree, null);
+
+ assertThat(cacheResolver).isNotNull();
+ assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache);
+
+ verify(this.mockCacheResolverOne, times(1)).resolve();
+ verify(this.mockCacheResolverTwo, times(1)).resolve();
+ verify(this.mockCacheResolverThree, never()).resolve();
+ }
+}