diff --git a/Compendium.sln b/Compendium.sln
index cdce03f..ffc591c 100644
--- a/Compendium.sln
+++ b/Compendium.sln
@@ -179,6 +179,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Sto
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Storage.Tests", "tests\Unit\Compendium.Abstractions.Storage.Tests\Compendium.Abstractions.Storage.Tests.csproj", "{585E082A-E775-426B-BEA5-D41D3AF53DE7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Caching", "src\Abstractions\Compendium.Abstractions.Caching\Compendium.Abstractions.Caching.csproj", "{4A6496B8-5833-428F-92D7-3EB331F5CA32}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Caching.Tests", "tests\Unit\Compendium.Abstractions.Caching.Tests\Compendium.Abstractions.Caching.Tests.csproj", "{262B8E91-B53C-49BC-833D-739DD4CFC2E1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -460,6 +464,14 @@ Global
{585E082A-E775-426B-BEA5-D41D3AF53DE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{585E082A-E775-426B-BEA5-D41D3AF53DE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{585E082A-E775-426B-BEA5-D41D3AF53DE7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A6496B8-5833-428F-92D7-3EB331F5CA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A6496B8-5833-428F-92D7-3EB331F5CA32}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A6496B8-5833-428F-92D7-3EB331F5CA32}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A6496B8-5833-428F-92D7-3EB331F5CA32}.Release|Any CPU.Build.0 = Release|Any CPU
+ {262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {262B8E91-B53C-49BC-833D-739DD4CFC2E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6}
@@ -543,5 +555,7 @@ Global
{D8DAEEF0-931B-4CE8-BEC4-767162B4CE7B} = {A2517BFF-352C-4123-81B2-2D848F3FB497}
{B4108100-52A7-4680-A0C4-7EB6955DB5F6} = {DE85A2F8-C0BA-47FD-8E9A-7D782FD6D3E2}
{585E082A-E775-426B-BEA5-D41D3AF53DE7} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
+ {4A6496B8-5833-428F-92D7-3EB331F5CA32} = {DE85A2F8-C0BA-47FD-8E9A-7D782FD6D3E2}
+ {262B8E91-B53C-49BC-833D-739DD4CFC2E1} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
EndGlobalSection
EndGlobal
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 11c00ee..9788d79 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,6 +18,8 @@
+
+
diff --git a/src/Abstractions/Compendium.Abstractions.Caching/CachingErrors.cs b/src/Abstractions/Compendium.Abstractions.Caching/CachingErrors.cs
new file mode 100644
index 0000000..40de478
--- /dev/null
+++ b/src/Abstractions/Compendium.Abstractions.Caching/CachingErrors.cs
@@ -0,0 +1,34 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+namespace Compendium.Abstractions.Caching;
+
+///
+/// Provides standardized error definitions for caching operations.
+///
+public static class CachingErrors
+{
+ ///
+ /// Gets the error code prefix for caching errors.
+ ///
+ public const string Prefix = "Caching";
+
+ ///
+ /// The supplied TTL was zero or negative.
+ ///
+ public static Error InvalidTtl(TimeSpan ttl) =>
+ Error.Validation(
+ $"{Prefix}.InvalidTtl",
+ $"TTL must be positive; received {ttl}.");
+
+ ///
+ /// The cache backend failed to satisfy the request and the failure is non-retriable
+ /// from the caller's perspective.
+ ///
+ public static Error BackendFailure(string message) =>
+ Error.Failure($"{Prefix}.BackendFailure", message);
+}
diff --git a/src/Abstractions/Compendium.Abstractions.Caching/Compendium.Abstractions.Caching.csproj b/src/Abstractions/Compendium.Abstractions.Caching/Compendium.Abstractions.Caching.csproj
new file mode 100644
index 0000000..abbf91b
--- /dev/null
+++ b/src/Abstractions/Compendium.Abstractions.Caching/Compendium.Abstractions.Caching.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Compendium.Abstractions.Caching
+ Compendium.Abstractions.Caching
+ Compendium.Abstractions.Caching
+ Caching abstractions for Compendium Framework: ICache port for provider-agnostic key/value caching (Get / Set / Remove / Exists) with TTL.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Abstractions/Compendium.Abstractions.Caching/GlobalUsings.cs b/src/Abstractions/Compendium.Abstractions.Caching/GlobalUsings.cs
new file mode 100644
index 0000000..d2393ca
--- /dev/null
+++ b/src/Abstractions/Compendium.Abstractions.Caching/GlobalUsings.cs
@@ -0,0 +1,8 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+global using Compendium.Core.Results;
diff --git a/src/Abstractions/Compendium.Abstractions.Caching/ICache.cs b/src/Abstractions/Compendium.Abstractions.Caching/ICache.cs
new file mode 100644
index 0000000..e40444a
--- /dev/null
+++ b/src/Abstractions/Compendium.Abstractions.Caching/ICache.cs
@@ -0,0 +1,69 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+namespace Compendium.Abstractions.Caching;
+
+///
+/// Provides provider-agnostic key/value caching operations with TTL.
+/// Implementations may target backends such as in-memory dictionaries, Redis, Memcached,
+/// or distributed caches behind IDistributedCache.
+///
+///
+///
+/// Implementations should never throw for ordinary cache misses or transient backend failures;
+/// they MUST surface failures via instead. Argument-validation errors
+/// (null / whitespace key) are permitted to throw.
+///
+///
+/// Tenant isolation, key namespacing, and serialization are implementation concerns and
+/// MUST be transparent to callers.
+///
+///
+public interface ICache
+{
+ ///
+ /// Retrieves the cached value at , or null when no
+ /// live entry exists. A missing key is a successful result with a null value,
+ /// not a failure.
+ ///
+ /// The expected value type.
+ /// The cache key. Must be non-null and non-whitespace.
+ /// The cancellation token.
+ /// A result containing the cached value (or null), or an error.
+ Task> GetAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// Stores at , replacing any existing entry.
+ ///
+ /// The value type.
+ /// The cache key. Must be non-null and non-whitespace.
+ /// The value to store.
+ /// Optional absolute time-to-live. When null, the entry has no expiry.
+ /// The cancellation token.
+ /// A result indicating success or an error.
+ Task SetAsync(
+ string key,
+ T value,
+ TimeSpan? ttl = null,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes the entry at . Removing a missing key is a no-op success.
+ ///
+ /// The cache key. Must be non-null and non-whitespace.
+ /// The cancellation token.
+ /// A result indicating success or an error.
+ Task RemoveAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// Determines whether a live entry exists at .
+ ///
+ /// The cache key. Must be non-null and non-whitespace.
+ /// The cancellation token.
+ /// A result containing true when an entry exists, otherwise false; or an error.
+ Task> ExistsAsync(string key, CancellationToken cancellationToken = default);
+}
diff --git a/src/Infrastructure/Compendium.Infrastructure/Caching/InMemoryCache.cs b/src/Infrastructure/Compendium.Infrastructure/Caching/InMemoryCache.cs
new file mode 100644
index 0000000..13a834f
--- /dev/null
+++ b/src/Infrastructure/Compendium.Infrastructure/Caching/InMemoryCache.cs
@@ -0,0 +1,114 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+using Compendium.Abstractions.Caching;
+using Compendium.Multitenancy;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Compendium.Infrastructure.Caching;
+
+///
+/// In-memory implementation of backed by
+/// . Suitable for single-process scenarios, tests,
+/// and framework E2E samples. For multi-instance deployments, use a distributed
+/// adapter (e.g. Compendium.Adapters.Redis).
+///
+///
+/// Tenant isolation. When an with a non-empty
+/// is resolved from DI, every key is prefixed
+/// with {tenantId}: before being stored. With no tenant context (null or empty
+/// TenantId), keys are written verbatim. The prefix is applied transparently
+/// — callers always see the original key.
+/// Thread safety. is thread-safe, so this
+/// adapter is also thread-safe.
+/// Result contract. Cache-miss is a successful
+/// with a null value, never a failure. Argument validation (null/whitespace
+/// key) is permitted to throw .
+///
+public sealed class InMemoryCache : ICache
+{
+ private readonly IMemoryCache _memoryCache;
+ private readonly ITenantContext? _tenantContext;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying .
+ /// Optional tenant context used to scope keys per tenant.
+ public InMemoryCache(IMemoryCache memoryCache, ITenantContext? tenantContext = null)
+ {
+ _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
+ _tenantContext = tenantContext;
+ }
+
+ ///
+ public Task> GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(key);
+
+ var scopedKey = ScopeKey(key);
+
+ if (_memoryCache.TryGetValue(scopedKey, out var raw) && raw is T typed)
+ {
+ return Task.FromResult(Result.Success(typed));
+ }
+
+ return Task.FromResult(Result.Success(default));
+ }
+
+ ///
+ public Task SetAsync(
+ string key,
+ T value,
+ TimeSpan? ttl = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(key);
+
+ if (ttl.HasValue && ttl.Value <= TimeSpan.Zero)
+ {
+ return Task.FromResult(Result.Failure(CachingErrors.InvalidTtl(ttl.Value)));
+ }
+
+ var options = new MemoryCacheEntryOptions();
+ if (ttl.HasValue)
+ {
+ options.AbsoluteExpirationRelativeToNow = ttl.Value;
+ }
+
+ _memoryCache.Set(ScopeKey(key), value, options);
+ return Task.FromResult(Result.Success());
+ }
+
+ ///
+ public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(key);
+
+ _memoryCache.Remove(ScopeKey(key));
+ return Task.FromResult(Result.Success());
+ }
+
+ ///
+ public Task> ExistsAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(key);
+
+ var exists = _memoryCache.TryGetValue(ScopeKey(key), out _);
+ return Task.FromResult(Result.Success(exists));
+ }
+
+ ///
+ /// Applies the active tenant's prefix to , or returns it unchanged
+ /// when no tenant context is bound.
+ ///
+ private string ScopeKey(string key)
+ {
+ var tenantId = _tenantContext?.TenantId;
+ return string.IsNullOrEmpty(tenantId) ? key : $"{tenantId}:{key}";
+ }
+}
diff --git a/src/Infrastructure/Compendium.Infrastructure/Caching/ServiceCollectionExtensions.cs b/src/Infrastructure/Compendium.Infrastructure/Caching/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..b95ece2
--- /dev/null
+++ b/src/Infrastructure/Compendium.Infrastructure/Caching/ServiceCollectionExtensions.cs
@@ -0,0 +1,48 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+using Compendium.Abstractions.Caching;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Compendium.Infrastructure.Caching;
+
+///
+/// DI extension methods for registering the in-memory adapter.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registers as the implementation,
+ /// backed by . The underlying is
+ /// also registered (via )
+ /// when no other registration exists.
+ ///
+ /// The service collection.
+ /// Optional configuration for the underlying .
+ /// The service collection for chaining.
+ public static IServiceCollection AddInMemoryCache(
+ this IServiceCollection services,
+ Action? configure = null)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ if (configure is null)
+ {
+ services.AddMemoryCache();
+ }
+ else
+ {
+ services.AddMemoryCache(configure);
+ }
+
+ services.TryAddSingleton();
+
+ return services;
+ }
+}
diff --git a/src/Infrastructure/Compendium.Infrastructure/Compendium.Infrastructure.csproj b/src/Infrastructure/Compendium.Infrastructure/Compendium.Infrastructure.csproj
index f3b404b..3983072 100644
--- a/src/Infrastructure/Compendium.Infrastructure/Compendium.Infrastructure.csproj
+++ b/src/Infrastructure/Compendium.Infrastructure/Compendium.Infrastructure.csproj
@@ -9,6 +9,7 @@
+
@@ -24,6 +25,8 @@
+
+
diff --git a/tests/Unit/Compendium.Abstractions.Caching.Tests/CachingErrorsTests.cs b/tests/Unit/Compendium.Abstractions.Caching.Tests/CachingErrorsTests.cs
new file mode 100644
index 0000000..b67e0eb
--- /dev/null
+++ b/tests/Unit/Compendium.Abstractions.Caching.Tests/CachingErrorsTests.cs
@@ -0,0 +1,43 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------
+
+namespace Compendium.Abstractions.Caching.Tests;
+
+public class CachingErrorsTests
+{
+ [Fact]
+ public void Prefix_IsCaching()
+ {
+ CachingErrors.Prefix.Should().Be("Caching");
+ }
+
+ [Fact]
+ public void InvalidTtl_ReturnsValidationErrorIncludingTtl()
+ {
+ // Arrange
+ var ttl = TimeSpan.FromSeconds(-5);
+
+ // Act
+ var error = CachingErrors.InvalidTtl(ttl);
+
+ // Assert
+ error.Code.Should().Be("Caching.InvalidTtl");
+ error.Type.Should().Be(ErrorType.Validation);
+ error.Message.Should().Contain(ttl.ToString());
+ }
+
+ [Fact]
+ public void BackendFailure_ReturnsFailureWithMessage()
+ {
+ // Act
+ var error = CachingErrors.BackendFailure("Redis unreachable");
+
+ // Assert
+ error.Code.Should().Be("Caching.BackendFailure");
+ error.Type.Should().Be(ErrorType.Failure);
+ error.Message.Should().Be("Redis unreachable");
+ }
+}
diff --git a/tests/Unit/Compendium.Abstractions.Caching.Tests/Compendium.Abstractions.Caching.Tests.csproj b/tests/Unit/Compendium.Abstractions.Caching.Tests/Compendium.Abstractions.Caching.Tests.csproj
new file mode 100644
index 0000000..74765e6
--- /dev/null
+++ b/tests/Unit/Compendium.Abstractions.Caching.Tests/Compendium.Abstractions.Caching.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ Compendium.Abstractions.Caching.Tests
+ Compendium.Abstractions.Caching.Tests
+ false
+ true
+ enable
+ enable
+ false
+ false
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/tests/Unit/Compendium.Abstractions.Caching.Tests/GlobalUsings.cs b/tests/Unit/Compendium.Abstractions.Caching.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..beac420
--- /dev/null
+++ b/tests/Unit/Compendium.Abstractions.Caching.Tests/GlobalUsings.cs
@@ -0,0 +1,12 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+//
+// -----------------------------------------------------------------------
+
+global using Xunit;
+global using FluentAssertions;
+global using NSubstitute;
+global using Compendium.Abstractions.Caching;
+global using Compendium.Core.Results;
diff --git a/tests/Unit/Compendium.Infrastructure.Tests/Caching/InMemoryCacheTests.cs b/tests/Unit/Compendium.Infrastructure.Tests/Caching/InMemoryCacheTests.cs
new file mode 100644
index 0000000..0072780
--- /dev/null
+++ b/tests/Unit/Compendium.Infrastructure.Tests/Caching/InMemoryCacheTests.cs
@@ -0,0 +1,344 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------
+
+using Compendium.Abstractions.Caching;
+using Compendium.Infrastructure.Caching;
+using Compendium.Multitenancy;
+using FluentAssertions;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+
+namespace Compendium.Infrastructure.Tests.Caching;
+
+public sealed class InMemoryCacheTests
+{
+ private static InMemoryCache CreateSut(out IMemoryCache memoryCache, ITenantContext? tenant = null)
+ {
+ memoryCache = new MemoryCache(new MemoryCacheOptions());
+ return new InMemoryCache(memoryCache, tenant);
+ }
+
+ [Fact]
+ public void Ctor_NullMemoryCache_Throws()
+ {
+ Action act = () => _ = new InMemoryCache(null!);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public async Task GetAsync_MissingKey_ReturnsSuccessWithNull()
+ {
+ var sut = CreateSut(out _);
+
+ var result = await sut.GetAsync("missing");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task SetAndGet_String_RoundTrip()
+ {
+ var sut = CreateSut(out _);
+
+ await sut.SetAsync("k", "hello");
+ var result = await sut.GetAsync("k");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be("hello");
+ }
+
+ [Fact]
+ public async Task SetAndGet_Int_RoundTrip()
+ {
+ var sut = CreateSut(out _);
+
+ await sut.SetAsync("n", 42);
+ var result = await sut.GetAsync("n");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be(42);
+ }
+
+ public sealed record Person(string Name, int Age);
+
+ [Fact]
+ public async Task SetAndGet_ComplexRecord_RoundTrip()
+ {
+ var sut = CreateSut(out _);
+ var person = new Person("Ada", 36);
+
+ await sut.SetAsync("person", person);
+ var result = await sut.GetAsync("person");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be(person);
+ }
+
+ [Fact]
+ public async Task GetAsync_WhenStoredTypeDiffers_ReturnsNull()
+ {
+ var sut = CreateSut(out _);
+
+ await sut.SetAsync("k", 42);
+
+ // Stored an int; requesting a string → contract returns null (not a failure).
+ var result = await sut.GetAsync("k");
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task SetAsync_ConfiguresAbsoluteExpirationRelativeToNow_OnEntryOptions()
+ {
+ // Arrange — substitute IMemoryCache so we can inspect the configured options
+ // without relying on wall-clock waits.
+ var memoryCache = Substitute.For();
+ var entry = Substitute.For();
+ entry.AbsoluteExpirationRelativeToNow.Returns((TimeSpan?)null);
+ memoryCache.CreateEntry(Arg.Any