🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 8 Min Lesezeit
0

How to Add Redis Caching Without Touching Your Business Logic

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

A practical guide to the Decorator Pattern in Go — and why "how many files did this change touch" is the real test of clean architecture.









The problem



Most Go backends end up with some version of this layering:




CODE
Handler → Service → Repository






At some point, a read-heavy piece of the system starts getting hit hard — a listing endpoint, a dashboard feed, anything queried far more often than it's written to. The obvious fix is caching. And the obvious first instinct is to open up the service layer and start adding Redis calls directly:




CODE
func (s *Service) GetAll(ctx context.Context) ([]Item, error) {
cached, err := s.redis.Get(ctx, "items:all")
if err == nil {
var list []Item
json.Unmarshal([]byte(cached), &list)
return list, nil
}

list, err := s.repo.GetAll(ctx)
// ...marshal, cache, return
}






It works. It's also the wrong instinct — and this is why.









Why "just add it to the service" is a trap



Once caching logic lives inside the service, every method that touches data starts accumulating cache-read, cache-write, and invalidation logic sitting right next to the actual business rules. A Create method that used to be a few lines of validation now also needs to know cache key formats, TTLs, and what to invalidate on write.



Two problems fall out of this immediately:



1. The service becomes hard to test in isolation. Testing that Create rejects invalid input now requires wiring up a fake Redis client too, even though the test has nothing to do with caching.



2. Caching becomes a leaky concern. A service's job is to enforce business rules and orchestrate. The moment it also knows how data is persisted-and-cached, two things that change for completely different reasons are now coupled in the same file. A TTL tweak and a validation rule change suddenly touch the same code.



Here's a useful test to apply to any architecture decision:




If this dependency were ripped out completely, how many files would need to change?




With caching embedded directly in the service, the answer is "the service itself, plus every test file for it." That's a smell. A well-isolated concern should be addable and removable as a localized, additive change — not a rewrite of things that have nothing to do with it.









The fix: caching is a decorator, not a service concern



The key insight: a cache-aside repository is still a repository. From the service's point of view, it doesn't matter whether a method hits the database directly or checks Redis first — it just needs something that satisfies the repository interface. So instead of teaching the service about Redis, build a second implementation of the repository interface that wraps the real one.




CODE
Service → Repository (interface)

┌────────┴────────┐
│ │
CachedRepository RealRepository
(checks Redis (talks to
first, falls the DB)
through to inner)






CachedRepository implements the exact same interface as the real database-backed implementation. It holds a reference to that real implementation internally and only calls it on a cache miss. The service never knows the difference — it's still just calling methods on a Repository.









Directory structure



A layout that keeps this separation honest looks roughly like this:




CODE
domain/
├── model.go # domain struct(s), sentinel errors
├── repository.go # Repository interface definition
├── service.go # business logic — validation, orchestration. Zero cache awareness.
├── cached_repository.go # decorator — implements Repository, wraps another Repository + cache

handler/
└── handler.go # HTTP layer — request binding, response mapping

repository/
└── postgres_repository.go # the real database implementation of Repository

cache/
└── client.go # generic, domain-agnostic cache wrapper (Get/Set/Delete/locks)






Two things worth calling out:





  • The cache client (cache/client.go) knows nothing about any specific domain. It's a thin, reusable wrapper — Get, Set, Delete, Exists, maybe a distributed lock. Any part of the system can use it. It has zero business logic in it.


  • The decorator (cached_repository.go) knows nothing about HTTP or database specifics. It only depends on the repository interface and the generic cache client. It's the one place where caching policy lives — key naming, TTLs, what gets invalidated on which write.



Transport (the cache client) is reusable across the whole application. Policy (the decorator) is specific to the domain it caches and lives right next to it — not buried in a shared file, not scattered across services.









The code






The interface everything depends on






CODE
type Repository interface {
Create(ctx context.Context, item *Item) error
GetAll(ctx context.Context) ([]Item, error)
GetByID(ctx context.Context, id uuid.UUID) (*Item, error)
Delete(ctx context.Context, id uuid.UUID) error
}






Nothing unusual here — this is exactly the interface a plain database-backed repository would implement with no caching involved at all. That's the point: introducing caching does not change this interface.






The service — completely unaware caching exists






CODE
type Service struct {
repo Repository
}

func NewService(repo Repository) *Service {
return &Service{repo: repo}
}

func (s *Service) Create(ctx context.Context, item *Item) error {
item.Name = strings.TrimSpace(item.Name)
if item.Name == "" {
return ErrEmptyName
}
return s.repo.Create(ctx, item)
}

func (s *Service) GetAll(ctx context.Context) ([]Item, error) {
return s.repo.GetAll(ctx)
}

func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
return s.repo.Delete(ctx, id)
}






There is no cache import in this file. No key naming, no TTL, nothing. If every trace of Redis were deleted from the codebase, this file would not need a single edit.






The decorator — where caching actually lives






CODE
type CachedRepository struct {
inner Repository // the real repository — called only on a cache miss
cache *cache.Client
}

func NewCachedRepository(inner Repository, c *cache.Client) *CachedRepository {
return &CachedRepository{inner: inner, cache: c}
}

const (
cacheVersion = "v1" // bump if the domain struct's shape ever changes
listTTL = 2 * time.Minute
)

func keyAll() string { return "items:" + cacheVersion + ":all" }

func (c *CachedRepository) GetAll(ctx context.Context) ([]Item, error) {
key := keyAll()

if list, ok := c.getList(ctx, key); ok {
return list, nil // cache hit — database never touched
}

list, err := c.inner.GetAll(ctx) // cache miss — fall through to the real repository
if err != nil {
return nil, err
}

c.setList(ctx, key, list, listTTL)
return list, nil
}

func (c *CachedRepository) Create(ctx context.Context, item *Item) error {
if err := c.inner.Create(ctx, item); err != nil {
return err
}
c.invalidate(ctx, keyAll())
return nil
}

func (c *CachedRepository) Delete(ctx context.Context, id uuid.UUID) error {
if err := c.inner.Delete(ctx, id); err != nil {
return err
}
c.invalidate(ctx, keyAll())
return nil
}

func (c *CachedRepository) invalidate(ctx context.Context, keys ...string) {
for _, k := range keys {
if err := c.cache.Delete(ctx, k); err != nil {
fmt.Printf("err: %v", err)
}
}
}






(getList/setList are small marshal/unmarshal helpers around the cache client — omitted here for brevity, but they're the only place encoding/json shows up in this file.)






Wiring — the only place that changes






CODE
// main.go — composition root
realRepo := repository.NewPostgresRepository(db)
cachedRepo := domain.NewCachedRepository(realRepo, cacheClient, logger)
svc := domain.NewService(cachedRepo) // ← service receives the cached one






That's the entire integration. Service is constructed exactly as it would be without caching — it just receives cachedRepo instead of realRepo. Since it's typed against the Repository interface either way, it has no way of knowing which one it got.









Why this is the actual point



Apply the test from earlier: if caching were ripped out entirely, how much would change?



With the decorator: delete cached_repository.go, change one line in main.go back to svc := domain.NewService(realRepo). Done. The handler, the service, the business-rule tests — none of it moves.



That's what clean architecture buys in practice. It's not about folder naming conventions or hitting some target number of layers — it's about the blast radius of a change matching its actual scope. Adding caching is a caching-shaped change. It shouldn't require editing files whose job is business rules.






What this means for testing




  • Service tests run against a mock Repository — no cache involved at all. Fast, minimal setup, and they test exactly one thing: business rules.

  • Decorator tests run independently: cache hit returns without touching the inner repository, cache miss falls through and populates the cache, writes invalidate the right keys. These tests carry zero business-rule logic — they only verify caching behavior.



Two test suites, two concerns, no overlap, no reason to touch one when changing the other.









The takeaway



The decorator pattern isn't a clever trick — it falls straight out of programming against interfaces instead of concrete types. Once a service depends on a Repository interface rather than a specific struct, anything satisfying that interface can be swapped in underneath it, including something that wraps another implementation of itself.



That's the difference between a caching layer and a caching tangle. And it's not specific to Redis — the same pattern applies to adding retries, metrics, logging, or any other cross-cutting concern to something that already works, without touching the thing itself.






Pattern applies regardless of stack — shown here in Go with a SQL-backed repository and Redis, but the shape is language-agnostic.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How to Add Redis Caching Without Touching Your Business Logic

Thematisch verwandte Begriffe: Redis, Caching, Without, Touching · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...