<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex E</title>
    <description>The latest articles on DEV Community by Alex E (@__2d3e61e).</description>
    <link>https://dev.to/__2d3e61e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3972169%2Fdeba24d7-a7da-4581-9c60-30514773514c.png</url>
      <title>DEV Community: Alex E</title>
      <link>https://dev.to/__2d3e61e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/__2d3e61e"/>
    <language>en</language>
    <item>
      <title>Why Squirix uses a strict client/server architecture for a .NET distributed cache</title>
      <dc:creator>Alex E</dc:creator>
      <pubDate>Sun, 07 Jun 2026 07:12:52 +0000</pubDate>
      <link>https://dev.to/__2d3e61e/why-squirix-uses-a-strict-clientserver-architecture-for-a-net-distributed-cache-5086</link>
      <guid>https://dev.to/__2d3e61e/why-squirix-uses-a-strict-clientserver-architecture-for-a-net-distributed-cache-5086</guid>
      <description>&lt;p&gt;Squirix 0.1.0 is an early preview of a .NET distributed cache. A typed client SDK talks to a remote server over gRPC; the server owns state, routing, durability, and operational endpoints.&lt;/p&gt;

&lt;p&gt;This is the direction I am validating in 0.1.0 — not a claim that every cache must work this way. Embedded designs are fine for many workloads. Squirix targets a different shape: &lt;strong&gt;the application stays a client; the server owns the data lifecycle.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just a cache library"
&lt;/h2&gt;

&lt;p&gt;A cache library is simple until you ask who owns what. When cache logic runs inside your app process, state, memory pressure, and persistence share the app's lifecycle. That works for local acceleration (&lt;code&gt;IMemoryCache&lt;/code&gt;), but gets ambiguous when you need shared state across instances, durability across restarts, independent health/metrics, or cluster routing.&lt;/p&gt;

&lt;p&gt;At that point you often have an implicit server with unclear boundaries. Squirix makes the split explicit from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded mode vs client/server mode
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Embedded&lt;/strong&gt; — cache logic in or tightly coupled to the app process: in-memory caches, libraries that hide remote I/O, or a co-located server called via direct references. Low friction; you rarely expect separate health probes or journal compaction on "just a dependency."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client/server&lt;/strong&gt; — the app holds no authoritative state. It connects over a wire contract; the server owns placement, mutations, durability, recovery, and admin/metrics endpoints. Redis and most production distributed caches follow this shape.&lt;/p&gt;

&lt;p&gt;Squirix 0.1.0 is client/server first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application -&amp;gt; Squirix client SDK -&amp;gt; Squirix.Server node(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two packages, enforced at build time: &lt;a href="https://www.nuget.org/packages/squirix/" rel="noopener noreferrer"&gt;&lt;code&gt;squirix&lt;/code&gt;&lt;/a&gt; (client) and &lt;code&gt;squirix.server&lt;/code&gt; (runtime). The server does not reference the client assembly.&lt;/p&gt;

&lt;p&gt;You can embed the server in ASP.NET Core via &lt;code&gt;AddSquirixServer&lt;/code&gt; / &lt;code&gt;MapSquirixServer&lt;/code&gt;, but application access still goes through &lt;code&gt;SquirixClient.ConnectAsync(...)&lt;/code&gt; — even in the same process. That is hosting convenience, not embedded cache semantics in the app layer.&lt;/p&gt;

&lt;p&gt;Embedded mode is not bad — it optimizes for different goals. Squirix targets shared remote state, server-owned durability, and operability as infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Squirix chooses client/server first
&lt;/h2&gt;

&lt;p&gt;Reasoning behind the 0.1.0 shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operational boundary&lt;/strong&gt; — deploy, probe, and upgrade cache nodes independently. Health (&lt;code&gt;/health/live&lt;/code&gt;, &lt;code&gt;/health/ready&lt;/code&gt;), admin (&lt;code&gt;/admin/whoami&lt;/code&gt;, &lt;code&gt;/admin/ring&lt;/code&gt;), and Prometheus at &lt;code&gt;/metrics&lt;/code&gt; live on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-owned lifecycle&lt;/strong&gt; — WAL journal, snapshots, compaction, and recovery stay in &lt;code&gt;squirix.server&lt;/code&gt;, not in every application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighter client package&lt;/strong&gt; — run the server as its own process or container; apps only reference the client SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustering path&lt;/strong&gt; — static consistent-hash routing in 0.1.0 is early, but routing and failover can evolve server-side without rewriting &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability isolation&lt;/strong&gt; — recovery and compaction failures stay out of application request threads while semantics harden.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this affects the public API
&lt;/h2&gt;

&lt;p&gt;You connect via &lt;code&gt;SquirixClient.ConnectAsync(...)&lt;/code&gt; and work with typed &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt; and explicit read results (&lt;code&gt;CacheValueResult&amp;lt;T&amp;gt;&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Squirix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SquirixClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"http://localhost:5001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetCacheAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lookup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production server hosting uses &lt;code&gt;AddSquirixServer&lt;/code&gt; / &lt;code&gt;MapSquirixServer&lt;/code&gt;. Transport is &lt;strong&gt;gRPC&lt;/strong&gt; (&lt;code&gt;SquirixCache.proto&lt;/code&gt;); cache operations, health, and admin routes are also available over &lt;strong&gt;HTTP/2 REST&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists in Squirix 0.1.0
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;0.1.0-preview.1&lt;/code&gt;&lt;/strong&gt;, .NET 10 only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client/server split (&lt;code&gt;squirix&lt;/code&gt; + &lt;code&gt;squirix.server&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Typed &lt;code&gt;ICache&amp;lt;T&amp;gt;&lt;/code&gt; — basic KV + expiration&lt;/li&gt;
&lt;li&gt;gRPC + HTTP/2 REST cache endpoints&lt;/li&gt;
&lt;li&gt;Per-node journal, snapshots, compaction, recovery&lt;/li&gt;
&lt;li&gt;Health, readiness, admin routes; Prometheus metrics; OpenTelemetry journal tracing&lt;/li&gt;
&lt;li&gt;Static consistent-hash single-owner routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local h2c dev: &lt;code&gt;$env:DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT = "1"&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still experimental
&lt;/h2&gt;

&lt;p&gt;Early preview — &lt;strong&gt;not production-ready&lt;/strong&gt;. API, wire format, and on-disk layouts may change during 0.x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/squirix/squirix" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/squirix/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/release-notes/v0.1.0.md" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/squirix/squirix/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/server-mode.md" rel="noopener noreferrer"&gt;Server mode&lt;/a&gt; · &lt;a href="https://github.com/squirix/squirix/blob/main/docs/diagnostics.md" rel="noopener noreferrer"&gt;Diagnostics&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>distributedsystems</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
