diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/caffeine/CaffeineHttpCacheStorage.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/caffeine/CaffeineHttpCacheStorage.java
new file mode 100644
index 0000000000..293c092f57
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/caffeine/CaffeineHttpCacheStorage.java
@@ -0,0 +1,146 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.cache.caffeine;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.github.benmanes.caffeine.cache.Cache;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.client5.http.impl.cache.AbstractSerializingCacheStorage;
+import org.apache.hc.client5.http.impl.cache.CacheConfig;
+import org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializer;
+import org.apache.hc.client5.http.impl.cache.NoopCacheEntrySerializer;
+import org.apache.hc.core5.util.Args;
+
+
+/**
+ * This class is a storage backend for cache entries that uses the
+ * Caffeine
+ * cache implementation.
+ *
+ * The size limits, eviction policy, and expiry policy are configured
+ * on the underlying Caffeine cache. The setting for
+ * {@link CacheConfig#getMaxCacheEntries()} is effectively ignored and
+ * should be enforced via the Caffeine configuration instead.
+ *
+ * Please refer to the Caffeine documentation for details on how to
+ * configure the cache itself.
+ *
+ * @since 5.6
+ */
+public class CaffeineHttpCacheStorage extends AbstractSerializingCacheStorage {
+
+ /**
+ * Creates cache that stores {@link HttpCacheStorageEntry}s without direct serialization.
+ *
+ * @since 5.6
+ */
+ public static CaffeineHttpCacheStorage createObjectCache(
+ final Cache cache, final CacheConfig config) {
+ return new CaffeineHttpCacheStorage<>(cache, config, NoopCacheEntrySerializer.INSTANCE);
+ }
+
+ /**
+ * Creates cache that stores serialized {@link HttpCacheStorageEntry}s.
+ *
+ * @since 5.6
+ */
+ public static CaffeineHttpCacheStorage createSerializedCache(
+ final Cache cache, final CacheConfig config) {
+ return new CaffeineHttpCacheStorage<>(cache, config, HttpByteArrayCacheEntrySerializer.INSTANCE);
+ }
+
+ private final Cache cache;
+
+ /**
+ * Constructs a storage backend using the provided Caffeine cache
+ * with the given configuration options, but using an alternative
+ * cache entry serialization strategy.
+ *
+ * @param cache where to store cached origin responses
+ * @param config cache storage configuration options - note that
+ * the setting for max object size and max entries
+ * should be configured on the Caffeine cache instead.
+ * @param serializer alternative serialization mechanism
+ */
+ public CaffeineHttpCacheStorage(
+ final Cache cache,
+ final CacheConfig config,
+ final HttpCacheEntrySerializer serializer) {
+ super((config != null ? config : CacheConfig.DEFAULT).getMaxUpdateRetries(),
+ Args.notNull(serializer, "Cache entry serializer"));
+ this.cache = Args.notNull(cache, "Caffeine cache");
+ }
+
+ @Override
+ protected String digestToStorageKey(final String key) {
+ return key;
+ }
+
+ @Override
+ protected void store(final String storageKey, final T storageObject) throws ResourceIOException {
+ cache.put(storageKey, storageObject);
+ }
+
+ @Override
+ protected T restore(final String storageKey) throws ResourceIOException {
+ return cache.getIfPresent(storageKey);
+ }
+
+ @Override
+ protected T getForUpdateCAS(final String storageKey) throws ResourceIOException {
+ return cache.getIfPresent(storageKey);
+ }
+
+ @Override
+ protected T getStorageObject(final T element) throws ResourceIOException {
+ return element;
+ }
+
+ @Override
+ protected boolean updateCAS(
+ final String storageKey, final T oldStorageObject, final T storageObject) throws ResourceIOException {
+ return cache.asMap().replace(storageKey, oldStorageObject, storageObject);
+ }
+
+ @Override
+ protected void delete(final String storageKey) throws ResourceIOException {
+ cache.invalidate(storageKey);
+ }
+
+ @Override
+ protected Map bulkRestore(final Collection storageKeys) throws ResourceIOException {
+ final Map present = cache.getAllPresent(storageKeys);
+ return new HashMap<>(present);
+ }
+
+}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/caffeine/TestCaffeineHttpCacheStorage.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/caffeine/TestCaffeineHttpCacheStorage.java
new file mode 100644
index 0000000000..5522264223
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/caffeine/TestCaffeineHttpCacheStorage.java
@@ -0,0 +1,187 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.cache.caffeine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.Resource;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.client5.http.impl.cache.CacheConfig;
+import org.apache.hc.client5.http.impl.cache.HeapResource;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.HeaderGroup;
+import org.junit.jupiter.api.Test;
+
+class TestCaffeineHttpCacheStorage {
+
+ private static HttpCacheEntry newEntry(final int status) throws ResourceIOException {
+ final Instant now = Instant.now();
+ final Header[] responseHeaders = new Header[0];
+ final Resource resource = new HeapResource(new byte[]{1, 2, 3});
+
+ final HeaderGroup requestHeaderGroup = new HeaderGroup();
+ final HeaderGroup responseHeaderGroup = new HeaderGroup();
+ responseHeaderGroup.setHeaders(responseHeaders);
+
+ // Use the non-deprecated @Internal constructor
+ return new HttpCacheEntry(
+ now,
+ now,
+ "GET",
+ "/",
+ requestHeaderGroup,
+ status,
+ responseHeaderGroup,
+ resource,
+ null);
+ }
+
+ private static CacheConfig newConfig() {
+ return CacheConfig.custom()
+ .setMaxUpdateRetries(3)
+ .build();
+ }
+
+ @Test
+ void testPutGetRemoveObjectCache() throws Exception {
+ final Cache cache = Caffeine.newBuilder().build();
+ final CacheConfig config = newConfig();
+ final CaffeineHttpCacheStorage storage =
+ CaffeineHttpCacheStorage.createObjectCache(cache, config);
+
+ final String key = "foo";
+ final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);
+
+ storage.putEntry(key, entry);
+
+ final HttpCacheEntry result = storage.getEntry(key);
+ assertNotNull(result);
+ assertEquals(HttpStatus.SC_OK, result.getStatus());
+
+ storage.removeEntry(key);
+ assertNull(storage.getEntry(key));
+ }
+
+ @Test
+ void testUpdateEntryObjectCache() throws Exception {
+ final Cache cache = Caffeine.newBuilder().build();
+ final CacheConfig config = newConfig();
+ final CaffeineHttpCacheStorage storage =
+ CaffeineHttpCacheStorage.createObjectCache(cache, config);
+
+ final String key = "bar";
+ final HttpCacheEntry original = newEntry(HttpStatus.SC_OK);
+ storage.putEntry(key, original);
+
+ final HttpCacheCASOperation casOperation = existing -> {
+ assertNotNull(existing);
+
+ final HeaderGroup requestHeaderGroup = new HeaderGroup();
+ requestHeaderGroup.setHeaders(existing.requestHeaders().getHeaders());
+
+ final HeaderGroup responseHeaderGroup = new HeaderGroup();
+ responseHeaderGroup.setHeaders(existing.responseHeaders().getHeaders());
+
+ return new HttpCacheEntry(
+ existing.getRequestInstant(),
+ existing.getResponseInstant(),
+ existing.getRequestMethod(),
+ existing.getRequestURI(),
+ requestHeaderGroup,
+ HttpStatus.SC_NOT_MODIFIED,
+ responseHeaderGroup,
+ existing.getResource(),
+ existing.getVariants());
+ };
+
+ storage.updateEntry(key, casOperation);
+
+ final HttpCacheEntry updated = storage.getEntry(key);
+ assertNotNull(updated);
+ assertEquals(HttpStatus.SC_NOT_MODIFIED, updated.getStatus());
+ }
+
+ @Test
+ void testGetEntriesUsesBulkRestore() throws Exception {
+ final Cache cache = Caffeine.newBuilder().build();
+ final CacheConfig config = newConfig();
+ final CaffeineHttpCacheStorage storage =
+ CaffeineHttpCacheStorage.createObjectCache(cache, config);
+
+ final HttpCacheEntry entry1 = newEntry(HttpStatus.SC_OK);
+ final HttpCacheEntry entry2 = newEntry(HttpStatus.SC_CREATED);
+
+ storage.putEntry("k1", entry1);
+ storage.putEntry("k2", entry2);
+
+ final Map result =
+ storage.getEntries(Arrays.asList("k1", "k2", "k3"));
+
+ assertEquals(2, result.size());
+ assertEquals(HttpStatus.SC_OK, result.get("k1").getStatus());
+ assertEquals(HttpStatus.SC_CREATED, result.get("k2").getStatus());
+ assertFalse(result.containsKey("k3"));
+ }
+
+ @Test
+ void testSerializedCacheStoresBytes() throws Exception {
+ final Cache cache = Caffeine.newBuilder().build();
+ final CacheConfig config = newConfig();
+ final CaffeineHttpCacheStorage storage =
+ CaffeineHttpCacheStorage.createSerializedCache(cache, config);
+
+ final String key = "baz";
+ final HttpCacheEntry entry = newEntry(HttpStatus.SC_OK);
+
+ storage.putEntry(key, entry);
+
+ // Underlying cache should contain serialized bytes
+ final byte[] stored = cache.getIfPresent(key);
+ assertNotNull(stored);
+ assertTrue(stored.length > 0);
+
+ final HttpCacheEntry result = storage.getEntry(key);
+ assertNotNull(result);
+ assertEquals(HttpStatus.SC_OK, result.getStatus());
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 4d5bc79145..809d7b734e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,6 +84,7 @@
1.5.2
1.52.0
1.26.2
+ 2.9.3
@@ -257,6 +258,11 @@
${brotli4j.version}
true
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+