diff --git a/examples/coalescing-bulkloader/pom.xml b/examples/coalescing-bulkloader/pom.xml
new file mode 100644
index 0000000000..3b118e638d
--- /dev/null
+++ b/examples/coalescing-bulkloader/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+ org.github.benmanes.caffeine.examples
+ coalescing-bulkloader
+ 1.0-SNAPSHOT
+
+
+ 1.8
+ 2.7.0
+ 4.12
+ 3.1.6
+
+
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+
+
+ junit
+ junit
+ ${junit.version}
+
+
+ org.awaitility
+ awaitility
+ ${awaitility.version}
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ ${java.version}
+
+
+
+
+
diff --git a/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java b/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java
new file mode 100644
index 0000000000..5c2087e89b
--- /dev/null
+++ b/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2019 Guus C. Bloemsma. All Rights Reserved.
+ *
+ * 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
+ *
+ * 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 com.github.benmanes.caffeine.examples.coalescing.bulkloader;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.stream.Collectors.toMap;
+
+import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * An implementation of {@link AsyncCacheLoader} that delays fetching a bit until "enough" keys are collected
+ * to do a bulk call. The assumption is that doing a bulk call is so much more efficient that it is worth
+ * the wait.
+ *
+ * @param the type of the key in the cache
+ * @param the type of the value in the cache
+ * @author complain to: guus@bloemsma.net
+ */
+public class CoalescingBulkloader implements AsyncCacheLoader {
+ private final Consumer> bulkLoader;
+ private int maxLoadSize; // maximum number of keys to load in one call
+ private long maxDelay; // maximum time between request of a value and loading it
+ private volatile Queue waitingKeys = new ConcurrentLinkedQueue<>();
+ private ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
+ private ScheduledFuture> schedule;
+ // Queue.size() is expensive, so here we keep track of the queue size separately
+ private AtomicInteger size = new AtomicInteger(0);
+
+ private final class WaitingKey {
+ Key key;
+ CompletableFuture future;
+ long waitingSince;
+ }
+
+ /**
+ * Wraps a bulk loader that returns values in the same order as the keys.
+ *
+ * @param maxLoadSize Maximum number of keys per bulk load
+ * @param maxDelay Maximum time to wait before bulk load is executed
+ * @param load Loader that takes keys and returns a future with the values in the same order as the keys.
+ * Extra values are ignored. Missing values lead to a {@link java.util.NoSuchElementException}
+ * for the corresponding future.
+ */
+ public static CoalescingBulkloader byOrder(int maxLoadSize, long maxDelay,
+ final Function, CompletableFuture>> load) {
+ return new CoalescingBulkloader<>(maxLoadSize, maxDelay, toLoad -> {
+ final Stream keys = toLoad.stream().map(wk -> wk.key);
+ load.apply(keys).thenAccept(values -> {
+ final Iterator iv = values.iterator();
+ for (CoalescingBulkloader.WaitingKey waitingKey : toLoad) {
+ if (iv.hasNext())
+ waitingKey.future.complete(iv.next());
+ else
+ waitingKey.future.completeExceptionally(new NoSuchElementException("No value for key " + waitingKey.key));
+ }
+ });
+ });
+ }
+
+ /**
+ * Wraps a bulk loader that returns values in a map accessed by key.
+ *
+ * @param maxLoadSize Maximum number of keys per bulk load
+ * @param maxDelay Maximum time to wait before bulk load is executed
+ * @param load Loader that takes keys and returns a future with a map with keys and values.
+ * Extra values are ignored. Missing values lead to a {@link java.util.NoSuchElementException}
+ * for the corresponding future.
+ */
+ public static CoalescingBulkloader byMap(int maxLoadSize, long maxDelay,
+ final Function, CompletableFuture