Redis for .NET Developers: Not Just Caching — Data Structures That Change System Design


 

Ask a .NET developer what Redis is, and the answer is usually:

“A distributed cache.”

That answer is technically correct, and architecturally incomplete.

Redis is not a faster dictionary you put in front of your database.
Redis is a server-side data structure engine, and once you start using it that way, your application designs change.

This article is the first in a Redis-focused stream for .NET developers. We’ll cover:

This is not a Redis command reference.
This is about thinking differently.


Why Redis Feels “Too Simple” at First

Redis:

To SQL-trained developers, this feels limiting.

In practice, it’s liberating.

Redis forces you to:

And in return, it gives you:


The Mental Shift: From Tables to Data Structures

In SQL, you ask:

“How do I query this data?”

In Redis, you ask:

“What operation do I need to be fast?”

That question determines the structure.


Strings: More Than Just Key–Value

Redis strings are binary-safe values up to 512 MB.

Use cases

Example: atomic counters

var count = await db.StringIncrementAsync("api:requests");

No locks. No race conditions. Atomic by default.

JSON payloads (simple case)

await db.StringSetAsync( "user:42", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(10));

Strings are the foundation — but rarely the best final form.


Hashes: Redis as a Document Store

Hashes store field–value pairs under one key.

Why hashes matter

  • partial updates

  • memory efficient

  • ideal for read models

Example: user profile

await db.HashSetAsync("user:42", new HashEntry[] { new("name", "Alice"), new("email", "alice@example.com"), new("status", "active") });

Read one field without deserializing everything:

var status = await db.HashGetAsync("user:42", "status");

Design insight

Hashes often replace:

  • small SQL tables

  • JSON blobs that change frequently


Sets: Unordered, Unique Collections

Sets enforce uniqueness automatically.

Use cases

  • memberships

  • permissions

  • feature flags per user

  • deduplication

Example: user roles

await db.SetAddAsync("user:42:roles", "admin"); await db.SetAddAsync("user:42:roles", "editor");

Check membership:

bool isAdmin = await db.SetContainsAsync("user:42:roles", "admin");

Why this changes design

No join tables. No indexes. No locking.


Sorted Sets: Where Redis Really Shines

Sorted sets (ZSETs) associate each member with a score.

Use cases

Example: leaderboard

await db.SortedSetAddAsync( "leaderboard", "user:42", score: 1234);

Top 10 users:

var top = await db.SortedSetRangeByRankAsync( "leaderboard", 0, 9, Order.Descending);

Time-window pattern

await db.SortedSetAddAsync( "events", eventId, DateTimeOffset.UtcNow.ToUnixTimeSeconds());

Then trim old entries:

await db.SortedSetRemoveRangeByScoreAsync( "events", double.NegativeInfinity, cutoff);

This replaces entire SQL queries.


Lists: Simple Queues (With Caveats)

Lists are ordered sequences.

Use cases

  • simple queues

  • task buffers

  • logs

Example

await db.ListLeftPushAsync("queue", payload); var item = await db.ListRightPopAsync("queue");

Lists work — but for serious eventing, Redis Streams are better.


Streams: Redis as an Event Log

Streams are append-only logs with IDs and consumer groups.

Why streams matter

  • persistent

  • replayable

  • support multiple consumers

  • built-in backpressure

Append event

await db.StreamAddAsync( "events", new NameValueEntry[] { new("type", "UserCreated"), new("userId", "42") });

Consumer groups enable:

  • fan-out

  • retries

  • exactly-once-ish processing

Streams are critical for CDC and event-driven architectures.


TTL Patterns: Where Most Redis Bugs Live

TTL is simple — until it isn’t.

Basic TTL

await db.StringSetAsync( "session:abc", data, TimeSpan.FromMinutes(30));

Pattern 1: cache-aside

  • read from Redis

  • on miss, load from DB

  • write back with TTL

Pattern 2: sliding expiration

await db.KeyExpireAsync("session:abc", TimeSpan.FromMinutes(30));

Pattern 3: negative caching

Cache “not found” to prevent DB hammering:

await db.StringSetAsync("user:999", "null", TimeSpan.FromSeconds(30));

Eviction Is Not Expiration

This is a common misunderstanding.

  • TTL: key expires predictably

  • Eviction: Redis removes keys under memory pressure

Your app must tolerate:

  • missing keys

  • unexpected evictions

Redis is a performance layer, not a source of truth.


Key Naming: The Hidden Design Tool

Good Redis usage lives or dies on key design.

Recommended pattern

{tenant}:{entity}:{id}:{aspect}

Example:

tenant1:user:42:profile tenant1:user:42:roles

This:

  • supports multi-tenancy

  • enables bulk deletes

  • improves observability


Redis vs PostgreSQL: Not Either/Or

Redis excels at:

  • fast reads

  • counters

  • ephemeral state

  • coordination

  • eventing

Postgres excels at:

  • durability

  • complex queries

  • transactions

  • reporting

The best systems use both.


Summary: Redis Changes How You Think

Once you stop treating Redis as “just a cache”, you start:

  • modelling problems differently

  • removing unnecessary queries

  • simplifying application logic

  • embracing explicit read models

Redis doesn’t replace your database.

It replaces accidental complexity.

Comments

Popular posts from this blog

Using a Semantic Model as a Reasoning Layer (Not Just Metadata)

A Thought Experiment: What If Analytics Models Were Semantic, Not Structural?

Dev Tunnels with Visual Studio 2022 and Visual Studio 2026