diff --git a/Elsa.sln b/Elsa.sln
index 73b357117..864a2eb11 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -250,6 +250,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience.Core", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience", "src\modules\Elsa.Resilience\Elsa.Resilience.csproj", "{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "caching", "caching", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching", "src\modules\Elsa.Caching\Elsa.Caching.csproj", "{6F14B066-DF7B-2409-59D8-CCCA90A4974C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -418,6 +422,10 @@ Global
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -489,6 +497,8 @@ Global
{CD7DC0D1-FFDC-417A-89BE-7F32408F583E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{70593549-8B26-4D63-9857-6BA8BB3E31DB} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E}
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E}
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {6F14B066-DF7B-2409-59D8-CCCA90A4974C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/src/modules/Elsa.Caching/Contracts/ICacheManager.cs b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs
new file mode 100644
index 000000000..6e93fe8c8
--- /dev/null
+++ b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs
@@ -0,0 +1,32 @@
+using Elsa.Caching.Options;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching;
+
+///
+/// A thin wrapper around , allowing for centralized handling of cache entries.
+///
+public interface ICacheManager
+{
+ ///
+ /// Provides options for configuring caching.
+ ///
+ IOptions CachingOptions { get; }
+
+ ///
+ /// Gets a change token for the specified key.
+ ///
+ IChangeToken GetToken(string key);
+
+ ///
+ /// Triggers the change token for the specified key.
+ ///
+ ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets an item from the cache, or creates it if it doesn't exist.
+ ///
+ Task GetOrCreateAsync(object key, Func> factory);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs
new file mode 100644
index 000000000..700da3d9d
--- /dev/null
+++ b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching;
+
+///
+/// Triggers the change token associated with the specified key.
+///
+public interface IChangeTokenSignalInvoker
+{
+ ///
+ /// Gets a change token for the specified key.
+ ///
+ IChangeToken GetToken(string key);
+
+ ///
+ /// Triggers the change token for the specified key.
+ ///
+ ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs
new file mode 100644
index 000000000..f937e2b63
--- /dev/null
+++ b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching;
+
+///
+/// Provides change tokens for memory caches, allowing code to evict cache entries by triggering a signal.
+///
+public interface IChangeTokenSignaler
+{
+ ///
+ /// Gets a change token for the specified key.
+ ///
+ IChangeToken GetToken(string key);
+
+ ///
+ /// Triggers the change token for the specified key.
+ ///
+ ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Elsa.Caching.csproj b/src/modules/Elsa.Caching/Elsa.Caching.csproj
new file mode 100644
index 000000000..ab4269db6
--- /dev/null
+++ b/src/modules/Elsa.Caching/Elsa.Caching.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+ Provides caching services and utility functions common to Elsa modules.
+
+ elsa module caching
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings b/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings
new file mode 100644
index 000000000..127787e89
--- /dev/null
+++ b/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs
new file mode 100644
index 000000000..879f9c9b7
--- /dev/null
+++ b/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs
@@ -0,0 +1,19 @@
+using Elsa.Caching.Features;
+using Elsa.Features.Services;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Extensions;
+
+///
+/// Provides methods to install and configure the distributed caching feature.
+///
+public static class ModuleExtensions
+{
+ ///
+ /// Adds the feature to the system.
+ ///
+ public static MemoryCacheFeature UseMemoryCache(this IModule module, Action? configure = default)
+ {
+ return module.Configure(configure);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs
new file mode 100644
index 000000000..1a3ad07cc
--- /dev/null
+++ b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs
@@ -0,0 +1,31 @@
+using Elsa.Caching.Options;
+using Elsa.Caching.Services;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Caching.Features;
+
+///
+/// Configures the MemoryCache.
+///
+public class MemoryCacheFeature(IModule module) : FeatureBase(module)
+{
+ ///
+ /// A delegate to configure the .
+ ///
+ public Action CachingOptions { get; set; } = _ => { };
+
+ ///
+ public override void Apply()
+ {
+ Services.Configure(CachingOptions);
+
+ Services
+ .AddMemoryCache()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ ;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/FodyWeavers.xml b/src/modules/Elsa.Caching/FodyWeavers.xml
new file mode 100644
index 000000000..00e1d9a1c
--- /dev/null
+++ b/src/modules/Elsa.Caching/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Options/CachingOptions.cs b/src/modules/Elsa.Caching/Options/CachingOptions.cs
new file mode 100644
index 000000000..4b9b5dd02
--- /dev/null
+++ b/src/modules/Elsa.Caching/Options/CachingOptions.cs
@@ -0,0 +1,12 @@
+namespace Elsa.Caching.Options;
+
+///
+/// Provides options for configuring caching.
+///
+public class CachingOptions
+{
+ ///
+ /// Gets or sets the duration for which cache entries are stored.
+ ///
+ public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(1);
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Services/CacheManager.cs b/src/modules/Elsa.Caching/Services/CacheManager.cs
new file mode 100644
index 000000000..6db501327
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/CacheManager.cs
@@ -0,0 +1,31 @@
+using Elsa.Caching.Options;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching.Services;
+
+///
+public class CacheManager(IMemoryCache memoryCache, IChangeTokenSignaler changeTokenSignaler, IOptions options) : ICacheManager
+{
+ ///
+ public IOptions CachingOptions => options;
+
+ ///
+ public IChangeToken GetToken(string key)
+ {
+ return changeTokenSignaler.GetToken(key);
+ }
+
+ ///
+ public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return changeTokenSignaler.TriggerTokenAsync(key, cancellationToken);
+ }
+
+ ///
+ public async Task GetOrCreateAsync(object key, Func> factory)
+ {
+ return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry));
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs b/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs
new file mode 100644
index 000000000..1bffad120
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs
@@ -0,0 +1,38 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching.Services;
+
+///
+public class ChangeTokenSignalInvoker : IChangeTokenSignalInvoker
+{
+ private readonly ConcurrentDictionary _changeTokens = new();
+
+ ///
+ public IChangeToken GetToken(string key)
+ {
+ return _changeTokens.GetOrAdd(
+ key,
+ _ =>
+ {
+ var cancellationTokenSource = new CancellationTokenSource();
+ var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
+ return new ChangeTokenInfo(changeToken, cancellationTokenSource);
+ }).ChangeToken;
+ }
+
+ ///
+ public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
+ {
+ if (_changeTokens.TryRemove(key, out var changeTokenInfo))
+ changeTokenInfo.TokenSource.Cancel();
+
+ return default;
+ }
+
+ private readonly struct ChangeTokenInfo(IChangeToken changeToken, CancellationTokenSource tokenSource)
+ {
+ public IChangeToken ChangeToken { get; } = changeToken;
+ public CancellationTokenSource TokenSource { get; } = tokenSource;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs b/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs
new file mode 100644
index 000000000..69d9f2fdd
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Primitives;
+
+namespace Elsa.Caching.Services;
+
+///
+public class ChangeTokenSignaler(IChangeTokenSignalInvoker invoker) : IChangeTokenSignaler
+{
+ ///
+ public IChangeToken GetToken(string key)
+ {
+ return invoker.GetToken(key);
+ }
+
+ ///
+ public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
+ {
+ return invoker.TriggerTokenAsync(key, cancellationToken);
+ }
+}
\ No newline at end of file