diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java
new file mode 100644
index 00000000..df385bd7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/Memoization.java
@@ -0,0 +1,155 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils.memoization;
+
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+
+
+/*******************************************************************************
+ ** Basic memoization functionality - with result timeouts (only when doing a get -
+ ** there's no cleanup thread), and max-size.
+ *******************************************************************************/
+public class Memoization
+{
+ private static final QLogger LOG = QLogger.getLogger(Memoization.class);
+
+ private final Map> map = Collections.synchronizedMap(new LinkedHashMap<>());
+
+ private Duration timeout = Duration.ofSeconds(600);
+ private Integer maxSize = 1000;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Memoization()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Optional getResult(K key)
+ {
+ MemoizedResult result = map.get(key);
+ if(result != null)
+ {
+ if(result.getTime().isAfter(Instant.now().minus(timeout)))
+ {
+ return (Optional.of(result.getResult()));
+ }
+ }
+
+ return (Optional.empty());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void storeResult(K key, V value)
+ {
+ map.put(key, new MemoizedResult<>(value));
+
+ //////////////////////////////////////
+ // make sure map didn't get too big //
+ // do this thread safely, please //
+ //////////////////////////////////////
+ try
+ {
+ if(map.size() > maxSize)
+ {
+ synchronized(map)
+ {
+ Iterator>> iterator = null;
+ while(map.size() > maxSize)
+ {
+ if(iterator == null)
+ {
+ iterator = map.entrySet().iterator();
+ }
+
+ if(iterator.hasNext())
+ {
+ iterator.next();
+ iterator.remove();
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error managing size of a Memoization", e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for timeoutSeconds
+ **
+ *******************************************************************************/
+ public void setTimeout(Duration timeout)
+ {
+ this.timeout = timeout;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for maxSize
+ **
+ *******************************************************************************/
+ public void setMaxSize(Integer maxSize)
+ {
+ this.maxSize = maxSize;
+ }
+
+
+
+ /*******************************************************************************
+ ** package-private - for tests to look at the map.
+ **
+ *******************************************************************************/
+ Map> getMap()
+ {
+ return map;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java
new file mode 100644
index 00000000..ba50211e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizedResult.java
@@ -0,0 +1,70 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils.memoization;
+
+
+import java.time.Instant;
+
+
+/*******************************************************************************
+ ** Object stored in the Memoization class. Shouldn't need to be visible outside
+ ** its package.
+ *******************************************************************************/
+class MemoizedResult
+{
+ private T result;
+ private Instant time;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MemoizedResult(T result)
+ {
+ this.result = result;
+ this.time = Instant.now();
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for result
+ **
+ *******************************************************************************/
+ public T getResult()
+ {
+ return result;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for time
+ **
+ *******************************************************************************/
+ public Instant getTime()
+ {
+ return time;
+ }
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java
new file mode 100644
index 00000000..c6858168
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/memoization/MemoizationTest.java
@@ -0,0 +1,136 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils.memoization;
+
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.utils.SleepUtils;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/*******************************************************************************
+ ** Unit test for Memoization
+ *******************************************************************************/
+class MemoizationTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ Memoization memoization = new Memoization<>();
+ memoization.setMaxSize(3);
+ memoization.setTimeout(Duration.ofMillis(100));
+
+ assertThat(memoization.getResult("one")).isEmpty();
+ memoization.storeResult("one", 1);
+ assertThat(memoization.getResult("one")).isPresent().get().isEqualTo(1);
+
+ ////////////////////////////////////////////////////
+ // store 3 more results - this should force 1 out //
+ ////////////////////////////////////////////////////
+ memoization.storeResult("two", 2);
+ memoization.storeResult("three", 3);
+ memoization.storeResult("four", 4);
+ assertThat(memoization.getResult("one")).isEmpty();
+
+ //////////////////////////////////
+ // make sure others are present //
+ //////////////////////////////////
+ assertThat(memoization.getResult("two")).isPresent().get().isEqualTo(2);
+ assertThat(memoization.getResult("three")).isPresent().get().isEqualTo(3);
+ assertThat(memoization.getResult("four")).isPresent().get().isEqualTo(4);
+
+ /////////////////////////////////////////////////////////////
+ // wait more than the timeout, then make sure all are gone //
+ /////////////////////////////////////////////////////////////
+ SleepUtils.sleep(150, TimeUnit.MILLISECONDS);
+ assertThat(memoization.getResult("two")).isEmpty();
+ assertThat(memoization.getResult("three")).isEmpty();
+ assertThat(memoization.getResult("four")).isEmpty();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ @Disabled("Slow, so not for CI - but good to demonstrate thread-safety during dev")
+ void testMultiThread() throws InterruptedException, ExecutionException
+ {
+ Memoization memoization = new Memoization<>();
+ ExecutorService executorService = Executors.newFixedThreadPool(20);
+
+ List> futures = new ArrayList<>();
+
+ for(int i = 0; i < 20; i++)
+ {
+ int finalI = i;
+ futures.add(executorService.submit(() ->
+ {
+ System.out.println("Start " + finalI);
+ for(int n = 0; n < 1_000_000; n++)
+ {
+ memoization.storeResult(String.valueOf(n), n);
+ memoization.getResult(String.valueOf(n));
+
+ if(n % 100_000 == 0)
+ {
+ System.out.format("Thread %d at %,d\n", finalI, +n);
+ }
+ }
+ System.out.println("End " + finalI);
+ }));
+ }
+
+ while(!futures.isEmpty())
+ {
+ Iterator> iterator = futures.iterator();
+ while(iterator.hasNext())
+ {
+ Future> next = iterator.next();
+ if(next.isDone())
+ {
+ Object o = next.get();
+ iterator.remove();
+ }
+ }
+ }
+
+ System.out.println("All Done");
+ }
+
+}
\ No newline at end of file