You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/01/13 09:55:48 UTC

[james-project] 02/10: JAMES-2921 Propose an Hybrid blobStore

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

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 7cfa6fe9d5909b414ee61ac10e460f1024087884
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Jan 3 18:06:59 2020 +0700

    JAMES-2921 Propose an Hybrid blobStore
---
 .../apache/james/blob/union/HybridBlobStore.java   | 182 +++++++
 .../apache/james/blob/union/UnionBlobStore.java    | 223 --------
 .../james/blob/union/HybridBlobStoreTest.java      | 516 ++++++++++++++++++
 .../james/blob/union/UnionBlobStoreTest.java       | 604 ---------------------
 .../blobstore/BlobStoreChoosingConfiguration.java  |   6 +-
 .../modules/blobstore/BlobStoreChoosingModule.java |  10 +-
 .../BlobStoreChoosingConfigurationTest.java        |  14 +-
 .../blobstore/BlobStoreChoosingModuleTest.java     |  14 +-
 8 files changed, 720 insertions(+), 849 deletions(-)

diff --git a/server/blob/blob-union/src/main/java/org/apache/james/blob/union/HybridBlobStore.java b/server/blob/blob-union/src/main/java/org/apache/james/blob/union/HybridBlobStore.java
new file mode 100644
index 0000000..45d1813
--- /dev/null
+++ b/server/blob/blob-union/src/main/java/org/apache/james/blob/union/HybridBlobStore.java
@@ -0,0 +1,182 @@
+/****************************************************************
+ * 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.james.blob.union;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.ObjectNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Mono;
+
+public class HybridBlobStore implements BlobStore {
+    @FunctionalInterface
+    public interface RequireLowCost {
+        RequirePerforming lowCost(BlobStore blobStore);
+    }
+
+    @FunctionalInterface
+    public interface RequirePerforming {
+        Builder highPerformance(BlobStore blobStore);
+    }
+
+    public static class Builder {
+        private final BlobStore lowCostBlobStore;
+        private final BlobStore highPerformanceBlobStore;
+
+        Builder(BlobStore lowCostBlobStore, BlobStore highPerformanceBlobStore) {
+            this.lowCostBlobStore = lowCostBlobStore;
+            this.highPerformanceBlobStore = highPerformanceBlobStore;
+        }
+
+        public HybridBlobStore build() {
+            return new HybridBlobStore(
+                lowCostBlobStore,
+                highPerformanceBlobStore);
+        }
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(HybridBlobStore.class);
+    private static final int SIZE_THRESHOLD = 32 * 1024;
+
+    public static RequireLowCost builder() {
+        return lowCost -> highPerformance -> new Builder(lowCost, highPerformance);
+    }
+
+    private final BlobStore lowCostBlobStore;
+    private final BlobStore highPerformanceBlobStore;
+
+    private HybridBlobStore(BlobStore lowCostBlobStore, BlobStore highPerformanceBlobStore) {
+        this.lowCostBlobStore = lowCostBlobStore;
+        this.highPerformanceBlobStore = highPerformanceBlobStore;
+    }
+
+    @Override
+    public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
+        return selectBlobStore(storagePolicy, Mono.just(data.length > SIZE_THRESHOLD))
+            .flatMap(blobStore -> blobStore.save(bucketName, data, storagePolicy));
+    }
+
+    @Override
+    public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
+        Preconditions.checkNotNull(data);
+
+        BufferedInputStream bufferedInputStream = new BufferedInputStream(data, SIZE_THRESHOLD + 1);
+        return selectBlobStore(storagePolicy, Mono.fromCallable(() -> isItABigStream(bufferedInputStream)))
+            .flatMap(blobStore -> blobStore.save(bucketName, bufferedInputStream, storagePolicy));
+    }
+
+    private Mono<BlobStore> selectBlobStore(StoragePolicy storagePolicy, Mono<Boolean> largeData) {
+        switch (storagePolicy) {
+            case LOW_COST:
+                return Mono.just(lowCostBlobStore);
+            case SIZE_BASED:
+                return largeData.map(isLarge -> {
+                    if (isLarge) {
+                        return lowCostBlobStore;
+                    }
+                    return highPerformanceBlobStore;
+                });
+            case HIGH_PERFORMANCE:
+                return Mono.just(highPerformanceBlobStore);
+            default:
+                throw new RuntimeException("Unknown storage policy: " + storagePolicy);
+        }
+    }
+
+    private boolean isItABigStream(InputStream bufferedData) throws IOException {
+        bufferedData.mark(0);
+        bufferedData.skip(SIZE_THRESHOLD);
+        boolean isItABigStream = bufferedData.read() != -1;
+        bufferedData.reset();
+        return isItABigStream;
+    }
+
+    @Override
+    public BucketName getDefaultBucketName() {
+        Preconditions.checkState(
+            lowCostBlobStore.getDefaultBucketName()
+                .equals(highPerformanceBlobStore.getDefaultBucketName()),
+            "lowCostBlobStore and highPerformanceBlobStore doen't have same defaultBucketName which could lead to " +
+                "unexpected result when interact with other APIs");
+
+        return lowCostBlobStore.getDefaultBucketName();
+    }
+
+    @Override
+    public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
+        return Mono.defer(() -> highPerformanceBlobStore.readBytes(bucketName, blobId))
+            .onErrorResume(this::logAndReturnEmpty)
+            .switchIfEmpty(Mono.defer(() -> lowCostBlobStore.readBytes(bucketName, blobId)));
+    }
+
+    @Override
+    public InputStream read(BucketName bucketName, BlobId blobId) {
+        try {
+            return highPerformanceBlobStore.read(bucketName, blobId);
+        } catch (ObjectNotFoundException e) {
+            return lowCostBlobStore.read(bucketName, blobId);
+        } catch (Exception e) {
+            LOGGER.error("Error reading {} {} in {}, falling back to {}", bucketName, blobId, highPerformanceBlobStore, lowCostBlobStore);
+            return lowCostBlobStore.read(bucketName, blobId);
+        }
+    }
+
+    @Override
+    public Mono<Void> deleteBucket(BucketName bucketName) {
+        return Mono.defer(() -> lowCostBlobStore.deleteBucket(bucketName))
+            .and(highPerformanceBlobStore.deleteBucket(bucketName))
+            .onErrorResume(this::logDeleteFailureAndReturnEmpty);
+    }
+
+    @Override
+    public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
+        return Mono.defer(() -> lowCostBlobStore.delete(bucketName, blobId))
+            .and(highPerformanceBlobStore.delete(bucketName, blobId))
+            .onErrorResume(this::logDeleteFailureAndReturnEmpty);
+    }
+
+    private <T> Mono<T> logAndReturnEmpty(Throwable throwable) {
+        LOGGER.error("error happens from current blob store, fall back to lowCost blob store", throwable);
+        return Mono.empty();
+    }
+
+    private <T> Mono<T> logDeleteFailureAndReturnEmpty(Throwable throwable) {
+        LOGGER.error("Cannot delete from either lowCost or highPerformance blob store", throwable);
+        return Mono.empty();
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("lowCostBlobStore", lowCostBlobStore)
+            .add("highPerformanceBlobStore", highPerformanceBlobStore)
+            .toString();
+    }
+}
diff --git a/server/blob/blob-union/src/main/java/org/apache/james/blob/union/UnionBlobStore.java b/server/blob/blob-union/src/main/java/org/apache/james/blob/union/UnionBlobStore.java
deleted file mode 100644
index 37f7c8a..0000000
--- a/server/blob/blob-union/src/main/java/org/apache/james/blob/union/UnionBlobStore.java
+++ /dev/null
@@ -1,223 +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.james.blob.union;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PushbackInputStream;
-import java.util.Optional;
-import java.util.function.Function;
-
-import org.apache.james.blob.api.BlobId;
-import org.apache.james.blob.api.BlobStore;
-import org.apache.james.blob.api.BucketName;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.github.fge.lambdas.Throwing;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Preconditions;
-
-import reactor.core.publisher.Mono;
-
-public class UnionBlobStore implements BlobStore {
-
-    @FunctionalInterface
-    public interface StorageOperation<T> {
-        Mono<BlobId> save(BucketName bucketName, T data, StoragePolicy storagePolicy);
-    }
-
-    @FunctionalInterface
-    public interface RequireCurrent {
-        RequireLegacy current(BlobStore blobStore);
-    }
-
-    @FunctionalInterface
-    public interface RequireLegacy {
-        Builder legacy(BlobStore blobStore);
-    }
-
-    public static class Builder {
-        private final BlobStore currentBlobStore;
-        private final BlobStore legacyBlobStore;
-
-        Builder(BlobStore currentBlobStore, BlobStore legacyBlobStore) {
-            this.currentBlobStore = currentBlobStore;
-            this.legacyBlobStore = legacyBlobStore;
-        }
-
-        public UnionBlobStore build() {
-            return new UnionBlobStore(
-                currentBlobStore,
-                legacyBlobStore);
-        }
-    }
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(UnionBlobStore.class);
-    private static final int UNAVAILABLE = -1;
-
-    public static RequireCurrent builder() {
-        return current -> legacy -> new Builder(current, legacy);
-    }
-
-    private final BlobStore currentBlobStore;
-    private final BlobStore legacyBlobStore;
-
-    private UnionBlobStore(BlobStore currentBlobStore, BlobStore legacyBlobStore) {
-        this.currentBlobStore = currentBlobStore;
-        this.legacyBlobStore = legacyBlobStore;
-    }
-
-    @Override
-    public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
-        try {
-            return saveToCurrentFallbackIfFails(bucketName, data, storagePolicy,
-                currentBlobStore::save,
-                legacyBlobStore::save);
-        } catch (Exception e) {
-            LOGGER.error("exception directly happens while saving bytes data, fall back to legacy blob store", e);
-            return legacyBlobStore.save(bucketName, data, storagePolicy);
-        }
-    }
-
-    @Override
-    public Mono<BlobId> save(BucketName bucketName, String data, StoragePolicy storagePolicy) {
-        try {
-            return saveToCurrentFallbackIfFails(bucketName, data, storagePolicy,
-                currentBlobStore::save,
-                legacyBlobStore::save);
-        } catch (Exception e) {
-            LOGGER.error("exception directly happens while saving String data, fall back to legacy blob store", e);
-            return legacyBlobStore.save(bucketName, data, storagePolicy);
-        }
-    }
-
-    @Override
-    public BucketName getDefaultBucketName() {
-        Preconditions.checkState(
-            currentBlobStore.getDefaultBucketName()
-                .equals(legacyBlobStore.getDefaultBucketName()),
-            "currentBlobStore and legacyBlobStore doen't have same defaultBucketName which could lead to " +
-                "unexpected result when interact with other APIs");
-
-        return currentBlobStore.getDefaultBucketName();
-    }
-
-    @Override
-    public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
-        try {
-            return saveToCurrentFallbackIfFails(bucketName, data, storagePolicy,
-                currentBlobStore::save,
-                legacyBlobStore::save);
-        } catch (Exception e) {
-            LOGGER.error("exception directly happens while saving InputStream data, fall back to legacy blob store", e);
-            return legacyBlobStore.save(bucketName, data, storagePolicy);
-        }
-    }
-
-    @Override
-    public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
-        try {
-            return readBytesFallBackIfFailsOrEmptyResult(bucketName, blobId);
-        } catch (Exception e) {
-            LOGGER.error("exception directly happens while readBytes, fall back to legacy blob store", e);
-            return Mono.defer(() -> legacyBlobStore.readBytes(bucketName, blobId));
-        }
-    }
-
-    @Override
-    public InputStream read(BucketName bucketName, BlobId blobId) {
-        try {
-            return readFallBackIfEmptyResult(bucketName, blobId);
-        } catch (Exception e) {
-            LOGGER.error("exception directly happens while read, fall back to legacy blob store", e);
-            return legacyBlobStore.read(bucketName, blobId);
-        }
-    }
-
-    @Override
-    public Mono<Void> deleteBucket(BucketName bucketName) {
-        return Mono.defer(() -> currentBlobStore.deleteBucket(bucketName))
-            .and(legacyBlobStore.deleteBucket(bucketName))
-            .onErrorResume(this::logDeleteFailureAndReturnEmpty);
-    }
-
-    @Override
-    public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
-        return Mono.defer(() -> currentBlobStore.delete(bucketName, blobId))
-            .and(legacyBlobStore.delete(bucketName, blobId))
-            .onErrorResume(this::logDeleteFailureAndReturnEmpty);
-    }
-
-    private InputStream readFallBackIfEmptyResult(BucketName bucketName, BlobId blobId) {
-        return Optional.ofNullable(currentBlobStore.read(bucketName, blobId))
-            .map(PushbackInputStream::new)
-            .filter(Throwing.predicate(this::streamHasContent).sneakyThrow())
-            .<InputStream>map(Function.identity())
-            .orElseGet(() -> legacyBlobStore.read(bucketName, blobId));
-    }
-
-    @VisibleForTesting
-    boolean streamHasContent(PushbackInputStream pushBackIS) throws IOException {
-        int byteRead = pushBackIS.read();
-        if (byteRead != UNAVAILABLE) {
-            pushBackIS.unread(byteRead);
-            return true;
-        }
-        return false;
-    }
-
-    private Mono<byte[]> readBytesFallBackIfFailsOrEmptyResult(BucketName bucketName, BlobId blobId) {
-        return Mono.defer(() -> currentBlobStore.readBytes(bucketName, blobId))
-            .onErrorResume(this::logAndReturnEmpty)
-            .switchIfEmpty(legacyBlobStore.readBytes(bucketName, blobId));
-    }
-
-    private <T> Mono<BlobId> saveToCurrentFallbackIfFails(
-        BucketName bucketName,
-        T data,
-        StoragePolicy storagePolicy,
-        StorageOperation<T> currentSavingOperation,
-        StorageOperation<T> fallbackSavingOperationSupplier) {
-
-        return Mono.defer(() -> currentSavingOperation.save(bucketName, data, storagePolicy))
-            .onErrorResume(this::logAndReturnEmpty)
-            .switchIfEmpty(Mono.defer(() -> fallbackSavingOperationSupplier.save(bucketName, data, storagePolicy)));
-    }
-
-    private <T> Mono<T> logAndReturnEmpty(Throwable throwable) {
-        LOGGER.error("error happens from current blob store, fall back to legacy blob store", throwable);
-        return Mono.empty();
-    }
-
-    private <T> Mono<T> logDeleteFailureAndReturnEmpty(Throwable throwable) {
-        LOGGER.error("Cannot delete from either legacy or current blob store", throwable);
-        return Mono.empty();
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(this)
-            .add("currentBlobStore", currentBlobStore)
-            .add("legacyBlobStore", legacyBlobStore)
-            .toString();
-    }
-}
diff --git a/server/blob/blob-union/src/test/java/org/apache/james/blob/union/HybridBlobStoreTest.java b/server/blob/blob-union/src/test/java/org/apache/james/blob/union/HybridBlobStoreTest.java
new file mode 100644
index 0000000..97bb738
--- /dev/null
+++ b/server/blob/blob-union/src/test/java/org/apache/james/blob/union/HybridBlobStoreTest.java
@@ -0,0 +1,516 @@
+/****************************************************************
+ * 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.james.blob.union;
+
+import static org.apache.james.blob.api.BlobStore.StoragePolicy.LOW_COST;
+import static org.apache.james.blob.api.BlobStore.StoragePolicy.HIGH_PERFORMANCE;
+import static org.apache.james.blob.api.BlobStore.StoragePolicy.SIZE_BASED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BlobStoreContract;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.blob.api.ObjectNotFoundException;
+import org.apache.james.blob.api.ObjectStoreException;
+import org.apache.james.blob.memory.MemoryBlobStore;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.base.MoreObjects;
+
+import reactor.core.publisher.Mono;
+
+class HybridBlobStoreTest implements BlobStoreContract {
+
+    private static class FailingBlobStore implements BlobStore {
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, String data, StoragePolicy storagePolicy) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public BucketName getDefaultBucketName() {
+            return BucketName.DEFAULT;
+        }
+
+        @Override
+        public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public InputStream read(BucketName bucketName, BlobId blobId) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public Mono<Void> deleteBucket(BucketName bucketName) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                .toString();
+        }
+    }
+
+    private static class ThrowingBlobStore implements BlobStore {
+
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, String data, StoragePolicy storagePolicy) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public BucketName getDefaultBucketName() {
+            return BucketName.DEFAULT;
+        }
+
+        @Override
+        public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public InputStream read(BucketName bucketName, BlobId blobId) {
+            throw new RuntimeException("broken everywhere");
+        }
+
+        @Override
+        public Mono<Void> deleteBucket(BucketName bucketName) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
+            return Mono.error(new RuntimeException("broken everywhere"));
+        }
+
+        @Override
+        public String toString() {
+            return MoreObjects.toStringHelper(this)
+                .toString();
+        }
+    }
+
+    private static final HashBlobId.Factory BLOB_ID_FACTORY = new HashBlobId.Factory();
+    private static final String STRING_CONTENT = "blob content";
+    private static final byte [] BLOB_CONTENT = STRING_CONTENT.getBytes();
+
+    private MemoryBlobStore lowCostBlobStore;
+    private MemoryBlobStore highPerformanceBlobStore;
+    private HybridBlobStore hybridBlobStore;
+
+    @BeforeEach
+    void setup() {
+        lowCostBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+        highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+        hybridBlobStore = HybridBlobStore.builder()
+            .lowCost(lowCostBlobStore)
+            .highPerformance(highPerformanceBlobStore)
+            .build();
+    }
+
+    @Override
+    public BlobStore testee() {
+        return hybridBlobStore;
+    }
+
+    @Override
+    public BlobId.Factory blobIdFactory() {
+        return BLOB_ID_FACTORY;
+    }
+
+    @Nested
+    class StoragePolicyTests {
+        @Test
+        void saveShouldRelyOnLowCostWhenLowCost() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveShouldRelyOnPerformingWhenPerforming() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, HIGH_PERFORMANCE).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveShouldRelyOnPerformingWhenSizeBasedAndSmall() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, SIZE_BASED).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveShouldRelyOnLowCostWhenSizeBasedAndBig() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, TWELVE_MEGABYTES, SIZE_BASED).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .satisfies(Throwing.consumer(inputStream -> assertThat(inputStream.read()).isGreaterThan(0)));
+                softly.assertThatThrownBy(() -> highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveInputStreamShouldRelyOnLowCostWhenLowCost() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, new ByteArrayInputStream(BLOB_CONTENT), LOW_COST).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveInputStreamShouldRelyOnPerformingWhenPerforming() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, new ByteArrayInputStream(BLOB_CONTENT), HIGH_PERFORMANCE).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveInputStreamShouldRelyOnPerformingWhenSizeBasedAndSmall() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, new ByteArrayInputStream(BLOB_CONTENT), SIZE_BASED).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+                softly.assertThatThrownBy(() -> lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+
+        @Test
+        void saveInputStreamShouldRelyOnLowCostWhenSizeBasedAndBig() {
+            BlobId blobId = hybridBlobStore.save(BucketName.DEFAULT, new ByteArrayInputStream(TWELVE_MEGABYTES), SIZE_BASED).block();
+
+            SoftAssertions.assertSoftly(softly -> {
+                softly.assertThat(lowCostBlobStore.read(BucketName.DEFAULT, blobId))
+                    .satisfies(Throwing.consumer(inputStream -> assertThat(inputStream.read()).isGreaterThan(0)));
+                softly.assertThatThrownBy(() -> highPerformanceBlobStore.read(BucketName.DEFAULT, blobId))
+                    .isInstanceOf(ObjectNotFoundException.class);
+            });
+        }
+    }
+
+    @Nested
+    class LowCostSaveThrowsExceptionDirectly {
+        @Test
+        void saveShouldFailWhenException() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new ThrowingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+
+            assertThatThrownBy(() -> hybridBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block())
+                .isInstanceOf(RuntimeException.class);
+        }
+
+        @Test
+        void saveInputStreamShouldFailWhenException() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new ThrowingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+
+            assertThatThrownBy(() -> hybridBlobStore.save(hybridBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LOW_COST).block())
+                .isInstanceOf(RuntimeException.class);
+        }
+    }
+
+    @Nested
+    class LowCostSaveCompletesExceptionally {
+
+        @Test
+        void saveShouldFailWhenLowCostCompletedExceptionally() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new FailingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+
+            assertThatThrownBy(() -> hybridBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block())
+                .isInstanceOf(RuntimeException.class);
+        }
+
+        @Test
+        void saveInputStreamShouldFallBackToPerformingWhenLowCostCompletedExceptionally() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new FailingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+
+            assertThatThrownBy(() -> hybridBlobStore.save(hybridBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LOW_COST).block())
+                .isInstanceOf(RuntimeException.class);
+        }
+
+    }
+
+    @Nested
+    class LowCostReadThrowsExceptionDirectly {
+
+        @Test
+        void readShouldReturnFallbackToPerformingWhenLowCostGotException() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new ThrowingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+            BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+            assertThat(hybridBlobStore.read(hybridBlobStore.getDefaultBucketName(), blobId))
+                .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+        }
+
+        @Test
+        void readBytesShouldReturnFallbackToPerformingWhenLowCostGotException() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new ThrowingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+            BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+            assertThat(hybridBlobStore.readBytes(hybridBlobStore.getDefaultBucketName(), blobId).block())
+                .isEqualTo(BLOB_CONTENT);
+        }
+
+    }
+
+    @Nested
+    class LowCostReadCompletesExceptionally {
+
+        @Test
+        void readShouldReturnFallbackToPerformingWhenLowCostCompletedExceptionally() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new FailingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+            BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+            assertThat(hybridBlobStore.read(hybridBlobStore.getDefaultBucketName(), blobId))
+                .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+        }
+
+        @Test
+        void readBytesShouldReturnFallbackToPerformingWhenLowCostCompletedExceptionally() {
+            MemoryBlobStore highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
+            HybridBlobStore hybridBlobStore = HybridBlobStore.builder()
+                .lowCost(new FailingBlobStore())
+                .highPerformance(highPerformanceBlobStore)
+                .build();
+            BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+            assertThat(hybridBlobStore.readBytes(hybridBlobStore.getDefaultBucketName(), blobId).block())
+                .isEqualTo(BLOB_CONTENT);
+        }
+    }
+
+    @Test
+    void readShouldReturnFromLowCostWhenAvailable() {
+        BlobId blobId = lowCostBlobStore.save(lowCostBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+        assertThat(hybridBlobStore.read(hybridBlobStore.getDefaultBucketName(), blobId))
+            .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+    }
+
+    @Test
+    void readShouldReturnFromPerformingWhenLowCostNotAvailable() {
+        BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+        assertThat(hybridBlobStore.read(hybridBlobStore.getDefaultBucketName(), blobId))
+            .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
+    }
+
+    @Test
+    void readBytesShouldReturnFromLowCostWhenAvailable() {
+        BlobId blobId = lowCostBlobStore.save(lowCostBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+        assertThat(hybridBlobStore.readBytes(lowCostBlobStore.getDefaultBucketName(), blobId).block())
+            .isEqualTo(BLOB_CONTENT);
+    }
+
+    @Test
+    void readBytesShouldReturnFromPerformingWhenLowCostNotAvailable() {
+        BlobId blobId = highPerformanceBlobStore.save(hybridBlobStore.getDefaultBucketName(), BLOB_CONTENT, LOW_COST).block();
+
+        assertThat(hybridBlobStore.readBytes(hybridBlobStore.getDefaultBucketName(), blobId).block())
+            .isEqualTo(BLOB_CONTENT);
+    }
+
+    @Test
+    void deleteBucketShouldDeleteBothLowCostAndPerformingBuckets() {
+        BlobId blobId1 = highPerformanceBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+        BlobId blobId2 = lowCostBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+        hybridBlobStore.deleteBucket(BucketName.DEFAULT).block();
+
+        assertThatThrownBy(() -> highPerformanceBlobStore.readBytes(BucketName.DEFAULT, blobId1).block())
+            .isInstanceOf(ObjectStoreException.class);
+        assertThatThrownBy(() -> lowCostBlobStore.readBytes(BucketName.DEFAULT, blobId2).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteBucketShouldDeleteLowCostBucketEvenWhenPerformingDoesNotExist() {
+        BlobId blobId = lowCostBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+        hybridBlobStore.deleteBucket(BucketName.DEFAULT).block();
+
+        assertThatThrownBy(() -> lowCostBlobStore.readBytes(BucketName.DEFAULT, blobId).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteBucketShouldDeletePerformingBucketEvenWhenLowCostDoesNotExist() {
+        BlobId blobId = highPerformanceBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+        hybridBlobStore.deleteBucket(BucketName.DEFAULT).block();
+
+        assertThatThrownBy(() -> highPerformanceBlobStore.readBytes(BucketName.DEFAULT, blobId).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteBucketShouldNotThrowWhenLowCostAndPerformingBucketsDoNotExist() {
+        assertThatCode(() -> hybridBlobStore.deleteBucket(BucketName.DEFAULT).block())
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    void getDefaultBucketNameShouldThrowWhenBlobStoreDontShareTheSameDefaultBucketName() {
+        lowCostBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY, BucketName.of("lowCost"));
+        highPerformanceBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY, BucketName.of("highPerformance"));
+        hybridBlobStore = HybridBlobStore.builder()
+            .lowCost(lowCostBlobStore)
+            .highPerformance(highPerformanceBlobStore)
+            .build();
+
+        assertThatThrownBy(() -> hybridBlobStore.getDefaultBucketName())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    void deleteShouldDeleteBothLowCostAndPerformingBlob() {
+        BlobId blobId1 = hybridBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+        BlobId blobId2 = hybridBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, HIGH_PERFORMANCE).block();
+
+        hybridBlobStore.delete(BucketName.DEFAULT, blobId1).block();
+
+        assertThatThrownBy(() -> highPerformanceBlobStore.readBytes(BucketName.DEFAULT, blobId1).block())
+            .isInstanceOf(ObjectStoreException.class);
+        assertThatThrownBy(() -> lowCostBlobStore.readBytes(BucketName.DEFAULT, blobId2).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteShouldDeleteLowCostBlobEvenWhenPerformingDoesNotExist() {
+        BlobId blobId = lowCostBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+        hybridBlobStore.delete(BucketName.DEFAULT, blobId).block();
+
+        assertThatThrownBy(() -> lowCostBlobStore.readBytes(BucketName.DEFAULT, blobId).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteShouldDeletePerformingBlobEvenWhenLowCostDoesNotExist() {
+        BlobId blobId = highPerformanceBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LOW_COST).block();
+
+        hybridBlobStore.delete(BucketName.DEFAULT, blobId).block();
+
+        assertThatThrownBy(() -> highPerformanceBlobStore.readBytes(BucketName.DEFAULT, blobId).block())
+            .isInstanceOf(ObjectStoreException.class);
+    }
+
+    @Test
+    void deleteShouldNotThrowWhenLowCostAndPerformingBlobsDoNotExist() {
+        assertThatCode(() -> hybridBlobStore.delete(BucketName.DEFAULT, blobIdFactory().randomId()).block())
+            .doesNotThrowAnyException();
+    }
+}
diff --git a/server/blob/blob-union/src/test/java/org/apache/james/blob/union/UnionBlobStoreTest.java b/server/blob/blob-union/src/test/java/org/apache/james/blob/union/UnionBlobStoreTest.java
deleted file mode 100644
index e8ccd81..0000000
--- a/server/blob/blob-union/src/test/java/org/apache/james/blob/union/UnionBlobStoreTest.java
+++ /dev/null
@@ -1,604 +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.james.blob.union;
-
-import static org.apache.james.blob.api.BlobStore.StoragePolicy.LowCost;
-import static org.apache.james.blob.api.BlobStore.StoragePolicy.LowCost;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.PushbackInputStream;
-import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Stream;
-
-import org.apache.james.blob.api.BlobId;
-import org.apache.james.blob.api.BlobStore;
-import org.apache.james.blob.api.BlobStoreContract;
-import org.apache.james.blob.api.BucketName;
-import org.apache.james.blob.api.HashBlobId;
-import org.apache.james.blob.api.ObjectStoreException;
-import org.apache.james.blob.memory.MemoryBlobStore;
-import org.apache.james.util.StreamUtils;
-import org.assertj.core.api.SoftAssertions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import com.google.common.base.MoreObjects;
-import com.google.common.collect.ImmutableList;
-
-import reactor.core.publisher.Mono;
-
-class UnionBlobStoreTest implements BlobStoreContract {
-
-    private static class FailingBlobStore implements BlobStore {
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, String data, StoragePolicy storagePolicy) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public BucketName getDefaultBucketName() {
-            return BucketName.DEFAULT;
-        }
-
-        @Override
-        public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public InputStream read(BucketName bucketName, BlobId blobId) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public Mono<Void> deleteBucket(BucketName bucketName) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public String toString() {
-            return MoreObjects.toStringHelper(this)
-                .toString();
-        }
-    }
-
-    private static class ThrowingBlobStore implements BlobStore {
-
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, byte[] data, StoragePolicy storagePolicy) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, String data, StoragePolicy storagePolicy) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public BucketName getDefaultBucketName() {
-            return BucketName.DEFAULT;
-        }
-
-        @Override
-        public Mono<BlobId> save(BucketName bucketName, InputStream data, StoragePolicy storagePolicy) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public Mono<byte[]> readBytes(BucketName bucketName, BlobId blobId) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public InputStream read(BucketName bucketName, BlobId blobId) {
-            throw new RuntimeException("broken everywhere");
-        }
-
-        @Override
-        public Mono<Void> deleteBucket(BucketName bucketName) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
-            return Mono.error(new RuntimeException("broken everywhere"));
-        }
-
-        @Override
-        public String toString() {
-            return MoreObjects.toStringHelper(this)
-                .toString();
-        }
-    }
-
-    private static final HashBlobId.Factory BLOB_ID_FACTORY = new HashBlobId.Factory();
-    private static final String STRING_CONTENT = "blob content";
-    private static final byte [] BLOB_CONTENT = STRING_CONTENT.getBytes();
-
-    private MemoryBlobStore currentBlobStore;
-    private MemoryBlobStore legacyBlobStore;
-    private UnionBlobStore unionBlobStore;
-
-    @BeforeEach
-    void setup() {
-        currentBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-        legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-        unionBlobStore = UnionBlobStore.builder()
-            .current(currentBlobStore)
-            .legacy(legacyBlobStore)
-            .build();
-    }
-
-    @Override
-    public BlobStore testee() {
-        return unionBlobStore;
-    }
-
-    @Override
-    public BlobId.Factory blobIdFactory() {
-        return BLOB_ID_FACTORY;
-    }
-
-    @Nested
-    class CurrentSaveThrowsExceptionDirectly {
-
-        @Test
-        void saveShouldFallBackToLegacyWhenCurrentGotException() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new ThrowingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-                softly.assertThat(legacyBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-            });
-        }
-
-        @Test
-        void saveInputStreamShouldFallBackToLegacyWhenCurrentGotException() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new ThrowingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LowCost).block();
-
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-                softly.assertThat(legacyBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-            });
-        }
-    }
-
-    @Nested
-    class CurrentSaveCompletesExceptionally {
-
-        @Test
-        void saveShouldFallBackToLegacyWhenCurrentCompletedExceptionally() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new FailingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-                softly.assertThat(legacyBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-            });
-        }
-
-        @Test
-        void saveInputStreamShouldFallBackToLegacyWhenCurrentCompletedExceptionally() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new FailingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LowCost).block();
-
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-                softly.assertThat(legacyBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                    .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-            });
-        }
-
-    }
-
-    @Nested
-    class CurrentReadThrowsExceptionDirectly {
-
-        @Test
-        void readShouldReturnFallbackToLegacyWhenCurrentGotException() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new ThrowingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-        }
-
-        @Test
-        void readBytesShouldReturnFallbackToLegacyWhenCurrentGotException() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new ThrowingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            assertThat(unionBlobStore.readBytes(unionBlobStore.getDefaultBucketName(), blobId).block())
-                .isEqualTo(BLOB_CONTENT);
-        }
-
-    }
-
-    @Nested
-    class CurrentReadCompletesExceptionally {
-
-        @Test
-        void readShouldReturnFallbackToLegacyWhenCurrentCompletedExceptionally() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new FailingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-                .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-        }
-
-        @Test
-        void readBytesShouldReturnFallbackToLegacyWhenCurrentCompletedExceptionally() {
-            MemoryBlobStore legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY);
-            UnionBlobStore unionBlobStore = UnionBlobStore.builder()
-                .current(new FailingBlobStore())
-                .legacy(legacyBlobStore)
-                .build();
-            BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-            assertThat(unionBlobStore.readBytes(unionBlobStore.getDefaultBucketName(), blobId).block())
-                .isEqualTo(BLOB_CONTENT);
-        }
-    }
-
-    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
-    @Nested
-    class CurrentAndLegacyCouldNotComplete {
-
-
-        Stream<Function<UnionBlobStore, Mono<?>>> blobStoreOperationsReturnFutures() {
-            return Stream.of(
-                blobStore -> blobStore.save(blobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost),
-                blobStore -> blobStore.save(blobStore.getDefaultBucketName(), STRING_CONTENT, LowCost),
-                blobStore -> blobStore.save(blobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LowCost),
-                blobStore -> blobStore.readBytes(blobStore.getDefaultBucketName(), BLOB_ID_FACTORY.randomId()));
-        }
-
-        Stream<Function<UnionBlobStore, InputStream>> blobStoreOperationsNotReturnFutures() {
-            return Stream.of(
-                blobStore -> blobStore.read(blobStore.getDefaultBucketName(), BLOB_ID_FACTORY.randomId()));
-        }
-
-        Stream<Arguments> blobStoresCauseReturnExceptionallyFutures() {
-            List<UnionBlobStore> futureThrowingUnionBlobStores = ImmutableList.of(
-                UnionBlobStore.builder()
-                    .current(new ThrowingBlobStore())
-                    .legacy(new FailingBlobStore())
-                    .build(),
-                UnionBlobStore.builder()
-                    .current(new FailingBlobStore())
-                    .legacy(new ThrowingBlobStore())
-                    .build(),
-                UnionBlobStore.builder()
-                    .current(new FailingBlobStore())
-                    .legacy(new FailingBlobStore())
-                    .build());
-
-            return blobStoreOperationsReturnFutures()
-                .flatMap(blobStoreFunction -> futureThrowingUnionBlobStores
-                    .stream()
-                    .map(blobStore -> Arguments.of(blobStore, blobStoreFunction)));
-        }
-
-        Stream<Arguments> blobStoresCauseThrowExceptions() {
-            UnionBlobStore throwingUnionBlobStore = UnionBlobStore.builder()
-                .current(new ThrowingBlobStore())
-                .legacy(new ThrowingBlobStore())
-                .build();
-
-            return StreamUtils.flatten(
-                blobStoreOperationsReturnFutures()
-                    .map(blobStoreFunction -> Arguments.of(throwingUnionBlobStore, blobStoreFunction)),
-                blobStoreOperationsNotReturnFutures()
-                    .map(blobStoreFunction -> Arguments.of(throwingUnionBlobStore, blobStoreFunction)));
-        }
-
-        @ParameterizedTest
-        @MethodSource("blobStoresCauseThrowExceptions")
-        void operationShouldThrow(UnionBlobStore blobStoreThrowsException,
-                                  Function<UnionBlobStore, Mono<?>> blobStoreOperation) {
-            assertThatThrownBy(() -> blobStoreOperation.apply(blobStoreThrowsException).block())
-                .isInstanceOf(RuntimeException.class);
-        }
-
-        @ParameterizedTest
-        @MethodSource("blobStoresCauseReturnExceptionallyFutures")
-        void operationShouldReturnExceptionallyFuture(UnionBlobStore blobStoreReturnsExceptionallyFuture,
-                                                      Function<UnionBlobStore, Mono<?>> blobStoreOperation) {
-            Mono<?> mono = blobStoreOperation.apply(blobStoreReturnsExceptionallyFuture);
-            assertThatThrownBy(mono::block).isInstanceOf(RuntimeException.class);
-        }
-    }
-
-    @Test
-    void readShouldReturnFromCurrentWhenAvailable() {
-        BlobId blobId = currentBlobStore.save(currentBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-            .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-    }
-
-    @Test
-    void readShouldReturnFromLegacyWhenCurrentNotAvailable() {
-        BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThat(unionBlobStore.read(unionBlobStore.getDefaultBucketName(), blobId))
-            .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-    }
-
-    @Test
-    void readBytesShouldReturnFromCurrentWhenAvailable() {
-        BlobId blobId = currentBlobStore.save(currentBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThat(unionBlobStore.readBytes(currentBlobStore.getDefaultBucketName(), blobId).block())
-            .isEqualTo(BLOB_CONTENT);
-    }
-
-    @Test
-    void readBytesShouldReturnFromLegacyWhenCurrentNotAvailable() {
-        BlobId blobId = legacyBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThat(unionBlobStore.readBytes(unionBlobStore.getDefaultBucketName(), blobId).block())
-            .isEqualTo(BLOB_CONTENT);
-    }
-
-    @Test
-    void saveShouldWriteToCurrent() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThat(currentBlobStore.readBytes(currentBlobStore.getDefaultBucketName(), blobId).block())
-            .isEqualTo(BLOB_CONTENT);
-    }
-
-    @Test
-    void saveShouldNotWriteToLegacy() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), BLOB_CONTENT, LowCost).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(legacyBlobStore.getDefaultBucketName(), blobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void saveStringShouldWriteToCurrent() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), STRING_CONTENT, LowCost).block();
-
-        assertThat(currentBlobStore.readBytes(currentBlobStore.getDefaultBucketName(), blobId).block())
-            .isEqualTo(BLOB_CONTENT);
-    }
-
-    @Test
-    void saveStringShouldNotWriteToLegacy() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), STRING_CONTENT, LowCost).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(legacyBlobStore.getDefaultBucketName(), blobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void saveInputStreamShouldWriteToCurrent() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LowCost).block();
-
-        assertThat(currentBlobStore.readBytes(currentBlobStore.getDefaultBucketName(), blobId).block())
-            .isEqualTo(BLOB_CONTENT);
-    }
-
-    @Test
-    void saveInputStreamShouldNotWriteToLegacy() {
-        BlobId blobId = unionBlobStore.save(unionBlobStore.getDefaultBucketName(), new ByteArrayInputStream(BLOB_CONTENT), LowCost).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(legacyBlobStore.getDefaultBucketName(), blobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void streamHasContentShouldReturnTrueWhenStreamHasContent() throws Exception {
-        PushbackInputStream pushBackIS = new PushbackInputStream(new ByteArrayInputStream(BLOB_CONTENT));
-
-        assertThat(unionBlobStore.streamHasContent(pushBackIS))
-            .isTrue();
-    }
-
-    @Test
-    void streamHasContentShouldReturnFalseWhenStreamHasNoContent() throws Exception {
-        PushbackInputStream pushBackIS = new PushbackInputStream(new ByteArrayInputStream(new byte[0]));
-
-        assertThat(unionBlobStore.streamHasContent(pushBackIS))
-            .isFalse();
-    }
-
-    @Test
-    void streamHasContentShouldNotThrowWhenStreamHasNoContent() {
-        PushbackInputStream pushBackIS = new PushbackInputStream(new ByteArrayInputStream(new byte[0]));
-
-        assertThatCode(() -> unionBlobStore.streamHasContent(pushBackIS))
-            .doesNotThrowAnyException();
-    }
-
-    @Test
-    void streamHasContentShouldNotDrainPushBackStreamContent() throws Exception {
-        PushbackInputStream pushBackIS = new PushbackInputStream(new ByteArrayInputStream(BLOB_CONTENT));
-        unionBlobStore.streamHasContent(pushBackIS);
-
-        assertThat(pushBackIS)
-            .hasSameContentAs(new ByteArrayInputStream(BLOB_CONTENT));
-    }
-
-    @Test
-    void streamHasContentShouldKeepStreamEmptyWhenStreamIsEmpty() throws Exception {
-        PushbackInputStream pushBackIS = new PushbackInputStream(new ByteArrayInputStream(new byte[0]));
-        unionBlobStore.streamHasContent(pushBackIS);
-
-        assertThat(pushBackIS)
-            .hasSameContentAs(new ByteArrayInputStream(new byte[0]));
-    }
-
-    @Test
-    void deleteBucketShouldDeleteBothCurrentAndLegacyBuckets() {
-        BlobId legacyBlobId = legacyBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-        BlobId currentBlobId = currentBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.deleteBucket(BucketName.DEFAULT).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(BucketName.DEFAULT, legacyBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-        assertThatThrownBy(() -> currentBlobStore.readBytes(BucketName.DEFAULT, currentBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteBucketShouldDeleteCurrentBucketEvenWhenLegacyDoesNotExist() {
-        BlobId currentBlobId = currentBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.deleteBucket(BucketName.DEFAULT).block();
-
-        assertThatThrownBy(() -> currentBlobStore.readBytes(BucketName.DEFAULT, currentBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteBucketShouldDeleteLegacyBucketEvenWhenCurrentDoesNotExist() {
-        BlobId legacyBlobId = legacyBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.deleteBucket(BucketName.DEFAULT).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(BucketName.DEFAULT, legacyBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteBucketShouldNotThrowWhenCurrentAndLegacyBucketsDoNotExist() {
-        assertThatCode(() -> unionBlobStore.deleteBucket(BucketName.DEFAULT).block())
-            .doesNotThrowAnyException();
-    }
-
-    @Test
-    void getDefaultBucketNameShouldThrowWhenBlobStoreDontShareTheSameDefaultBucketName() {
-        currentBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY, BucketName.of("current"));
-        legacyBlobStore = new MemoryBlobStore(BLOB_ID_FACTORY, BucketName.of("legacy"));
-        unionBlobStore = UnionBlobStore.builder()
-            .current(currentBlobStore)
-            .legacy(legacyBlobStore)
-            .build();
-
-        assertThatThrownBy(() -> unionBlobStore.getDefaultBucketName())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-    @Test
-    void deleteShouldDeleteBothCurrentAndLegacyBlob() {
-        BlobId legacyBlobId = legacyBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-        BlobId currentBlobId = currentBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.delete(BucketName.DEFAULT, currentBlobId).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(BucketName.DEFAULT, legacyBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-        assertThatThrownBy(() -> currentBlobStore.readBytes(BucketName.DEFAULT, currentBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteShouldDeleteCurrentBlobEvenWhenLegacyDoesNotExist() {
-        BlobId currentBlobId = currentBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.delete(BucketName.DEFAULT, currentBlobId).block();
-
-        assertThatThrownBy(() -> currentBlobStore.readBytes(BucketName.DEFAULT, currentBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteShouldDeleteLegacyBlobEvenWhenCurrentDoesNotExist() {
-        BlobId legacyBlobId = legacyBlobStore.save(BucketName.DEFAULT, BLOB_CONTENT, LowCost).block();
-
-        unionBlobStore.delete(BucketName.DEFAULT, legacyBlobId).block();
-
-        assertThatThrownBy(() -> legacyBlobStore.readBytes(BucketName.DEFAULT, legacyBlobId).block())
-            .isInstanceOf(ObjectStoreException.class);
-    }
-
-    @Test
-    void deleteShouldNotThrowWhenCurrentAndLegacyBlobsDoNotExist() {
-        assertThatCode(() -> unionBlobStore.delete(BucketName.DEFAULT, blobIdFactory().randomId()).block())
-            .doesNotThrowAnyException();
-    }
-}
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfiguration.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfiguration.java
index bde669c..84bc7fb 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfiguration.java
+++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfiguration.java
@@ -34,7 +34,7 @@ public class BlobStoreChoosingConfiguration {
     public enum BlobStoreImplName {
         CASSANDRA("cassandra"),
         OBJECTSTORAGE("objectstorage"),
-        UNION("union");
+        HYBRID("hybrid");
 
         static String supportedImplNames() {
             return Stream.of(BlobStoreImplName.values())
@@ -82,8 +82,8 @@ public class BlobStoreChoosingConfiguration {
         return new BlobStoreChoosingConfiguration(BlobStoreImplName.OBJECTSTORAGE);
     }
 
-    public static BlobStoreChoosingConfiguration union() {
-        return new BlobStoreChoosingConfiguration(BlobStoreImplName.UNION);
+    public static BlobStoreChoosingConfiguration hybrid() {
+        return new BlobStoreChoosingConfiguration(BlobStoreImplName.HYBRID);
     }
 
     private final BlobStoreImplName implementation;
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingModule.java b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingModule.java
index 7011d1e..2ab354d 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingModule.java
+++ b/server/container/guice/cassandra-rabbitmq-guice/src/main/java/org/apache/james/modules/blobstore/BlobStoreChoosingModule.java
@@ -33,7 +33,7 @@ import org.apache.james.blob.api.MetricableBlobStore;
 import org.apache.james.blob.cassandra.CassandraBlobModule;
 import org.apache.james.blob.cassandra.CassandraBlobStore;
 import org.apache.james.blob.objectstorage.ObjectStorageBlobStore;
-import org.apache.james.blob.union.UnionBlobStore;
+import org.apache.james.blob.union.HybridBlobStore;
 import org.apache.james.modules.mailbox.ConfigurationComponent;
 import org.apache.james.modules.objectstorage.ObjectStorageDependenciesModule;
 import org.apache.james.utils.PropertiesProvider;
@@ -82,10 +82,10 @@ public class BlobStoreChoosingModule extends AbstractModule {
                 return swiftBlobStoreProvider.get();
             case CASSANDRA:
                 return cassandraBlobStoreProvider.get();
-            case UNION:
-                return UnionBlobStore.builder()
-                    .current(swiftBlobStoreProvider.get())
-                    .legacy(cassandraBlobStoreProvider.get())
+            case HYBRID:
+                return HybridBlobStore.builder()
+                    .lowCost(swiftBlobStoreProvider.get())
+                    .highPerformance(cassandraBlobStoreProvider.get())
                     .build();
             default:
                 throw new RuntimeException(String.format("can not get the right blobstore provider with configuration %s",
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfigurationTest.java b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfigurationTest.java
index a5e1eb1..2ef718d 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfigurationTest.java
+++ b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingConfigurationTest.java
@@ -31,7 +31,7 @@ class BlobStoreChoosingConfigurationTest {
 
     private static final String OBJECT_STORAGE = "objectstorage";
     private static final String CASSANDRA = "cassandra";
-    private static final String UNION = "union";
+    private static final String HYBRID = "hybrid";
 
     @Test
     void shouldMatchBeanContract() {
@@ -45,7 +45,7 @@ class BlobStoreChoosingConfigurationTest {
 
         assertThatThrownBy(() -> BlobStoreChoosingConfiguration.from(configuration))
             .isInstanceOf(IllegalStateException.class)
-            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, union");
+            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, hybrid");
     }
 
     @Test
@@ -55,7 +55,7 @@ class BlobStoreChoosingConfigurationTest {
 
         assertThatThrownBy(() -> BlobStoreChoosingConfiguration.from(configuration))
             .isInstanceOf(IllegalStateException.class)
-            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, union");
+            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, hybrid");
     }
 
     @Test
@@ -65,7 +65,7 @@ class BlobStoreChoosingConfigurationTest {
 
         assertThatThrownBy(() -> BlobStoreChoosingConfiguration.from(configuration))
             .isInstanceOf(IllegalStateException.class)
-            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, union");
+            .hasMessage("implementation property is missing please use one of supported values in: cassandra, objectstorage, hybrid");
     }
 
     @Test
@@ -75,7 +75,7 @@ class BlobStoreChoosingConfigurationTest {
 
         assertThatThrownBy(() -> BlobStoreChoosingConfiguration.from(configuration))
             .isInstanceOf(IllegalArgumentException.class)
-            .hasMessage("un_supported is not a valid name of BlobStores, please use one of supported values in: cassandra, objectstorage, union");
+            .hasMessage("un_supported is not a valid name of BlobStores, please use one of supported values in: cassandra, objectstorage, hybrid");
     }
 
     @Test
@@ -93,13 +93,13 @@ class BlobStoreChoosingConfigurationTest {
     @Test
     void fromShouldReturnConfigurationWhenBlobStoreImplIsUnion() {
         PropertiesConfiguration configuration = new PropertiesConfiguration();
-        configuration.addProperty("implementation", UNION);
+        configuration.addProperty("implementation", HYBRID);
 
         assertThat(
             BlobStoreChoosingConfiguration.from(configuration)
                 .getImplementation()
                 .getName())
-            .isEqualTo(UNION);
+            .isEqualTo(HYBRID);
     }
 
     @Test
diff --git a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingModuleTest.java b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingModuleTest.java
index ce4354b..4e04a5b 100644
--- a/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingModuleTest.java
+++ b/server/container/guice/cassandra-rabbitmq-guice/src/test/java/org/apache/james/modules/blobstore/BlobStoreChoosingModuleTest.java
@@ -27,7 +27,7 @@ import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.james.FakePropertiesProvider;
 import org.apache.james.blob.cassandra.CassandraBlobStore;
 import org.apache.james.blob.objectstorage.ObjectStorageBlobStore;
-import org.apache.james.blob.union.UnionBlobStore;
+import org.apache.james.blob.union.HybridBlobStore;
 import org.apache.james.modules.blobstore.BlobStoreChoosingConfiguration.BlobStoreImplName;
 import org.apache.james.modules.mailbox.ConfigurationComponent;
 import org.junit.jupiter.api.Test;
@@ -105,16 +105,16 @@ class BlobStoreChoosingModuleTest {
     }
 
     @Test
-    void provideChoosingConfigurationShouldReturnUnionConfigurationWhenConfigurationImplIsUnion() throws Exception {
+    void provideChoosingConfigurationShouldReturnHybridConfigurationWhenConfigurationImplIsHybrid() throws Exception {
         BlobStoreChoosingModule module = new BlobStoreChoosingModule();
         PropertiesConfiguration configuration = new PropertiesConfiguration();
-        configuration.addProperty("implementation", BlobStoreImplName.UNION.getName());
+        configuration.addProperty("implementation", BlobStoreImplName.HYBRID.getName());
         FakePropertiesProvider propertyProvider = FakePropertiesProvider.builder()
             .register(ConfigurationComponent.NAME, configuration)
             .build();
 
         assertThat(module.provideChoosingConfiguration(propertyProvider))
-            .isEqualTo(BlobStoreChoosingConfiguration.union());
+            .isEqualTo(BlobStoreChoosingConfiguration.hybrid());
     }
 
     @Test
@@ -149,11 +149,11 @@ class BlobStoreChoosingModuleTest {
     }
 
     @Test
-    void provideBlobStoreShouldReturnUnionBlobStoreWhenUnionConfigured() {
+    void provideBlobStoreShouldReturnHybridBlobStoreWhenHybridConfigured() {
         BlobStoreChoosingModule module = new BlobStoreChoosingModule();
 
-        assertThat(module.provideBlobStore(BlobStoreChoosingConfiguration.union(),
+        assertThat(module.provideBlobStore(BlobStoreChoosingConfiguration.hybrid(),
             CASSANDRA_BLOBSTORE_PROVIDER, OBJECT_STORAGE_BLOBSTORE_PROVIDER))
-            .isInstanceOf(UnionBlobStore.class);
+            .isInstanceOf(HybridBlobStore.class);
     }
 }
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org