# Adding a Caching Layer for Performance Optimization in Go

> Optimize Go application performance by adding a transparent caching layer for the golang-clean-architecture repository. Learn how to implement caching with a decorator pattern for faster read operations.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: performance
- Published: 2026-03-06

---

**You can add a transparent caching layer to the golang-clean-architecture repository by implementing a decorator pattern that wraps the `UserRepository` interface, checking an in-memory store before falling back to the database for read-heavy operations like `FindAll`.**

Adding a caching layer for performance optimization in Go is essential when working with read-heavy workloads in clean architecture projects. The `manakuro/golang-clean-architecture` repository provides an ideal foundation for this pattern because its strict separation between domain, use-case, and adapter layers allows you to inject cache decorators without modifying business logic. By wrapping the existing repository implementation with a caching decorator, you eliminate redundant database queries while maintaining the abstraction that the use-case layer depends on.

## Understanding the Clean Architecture Boundaries

The repository follows a strict Clean Architecture layout that makes caching straightforward to implement. The use-case layer depends only on interfaces, never concrete implementations.

### Domain and Repository Interfaces

The contract between business logic and data access lives in [`pkg/usecase/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/repository/user.go). This interface defines the methods that any user repository must implement, including `FindAll` for fetching records and `Create` for inserting them. Because the `UserUsecase` constructor in [`pkg/usecase/usecase/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/usecase/usecase/user.go) accepts this interface via `NewUserUsecase`, you can substitute the concrete GORM repository with a cached version without touching the business logic.

### Concrete Implementations

The actual database logic resides in [`pkg/adapter/repository/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user.go), which implements the `UserRepository` interface using GORM. This concrete type queries the MySQL database directly, making it the perfect target for caching optimization.

## Implementing the Cache Contract

A cache sits between the use-case and repository layers. You define a minimal contract that supports get, set, and delete operations, then implement it using an in-memory store or external system like Redis.

### Cache Interface Definition

Create [`pkg/cache/cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/cache/cache.go) to define the contract that all cache implementations must satisfy:

```go
package cache

type Cache interface {
    // Get fetches a value by key; returns (value, true) if present.
    Get(key string) (interface{}, bool)
    // Set stores a value with an expiration.
    Set(key string, value interface{}, ttlSeconds int)
    // Delete removes a key.
    Delete(key string)
}

```

### In-Memory Implementation

For local development or single-instance deployments, implement the interface using `github.com/patrickmn/go-cache` in [`pkg/cache/memory.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/cache/memory.go):

```go
package cache

import (
    "time"

    "github.com/patrickmn/go-cache"
)

type memoryCache struct {
    c *cache.Cache
}

func NewMemoryCache(defaultTTL, cleanupInterval time.Duration) Cache {
    return &memoryCache{c: cache.New(defaultTTL, cleanupInterval)}
}

func (m *memoryCache) Get(key string) (interface{}, bool) { 
    return m.c.Get(key) 
}

func (m *memoryCache) Set(key string, value interface{}, ttl int) {
    m.c.Set(key, value, time.Duration(ttl)*time.Second)
}

func (m *memoryCache) Delete(key string) { 
    m.c.Delete(key) 
}

```

## Creating the Cache Decorator

The decorator pattern allows you to add caching behavior without modifying the existing GORM repository. You create a new struct that embeds both the original repository and the cache, implementing the same `UserRepository` interface.

### Wrapping the UserRepository

Create [`pkg/adapter/repository/user_cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user_cache.go) to implement the caching logic:

```go
package repository

import (
    "encoding/json"
    "fmt"
    
    "golang-clean-architecture/pkg/cache"
    "golang-clean-architecture/pkg/domain/model"
    "golang-clean-architecture/pkg/usecase/repository"
)

type cachedUserRepository struct {
    repo  repository.UserRepository
    cache cache.Cache
}

// NewCachedUserRepository wraps a concrete repo with caching.
func NewCachedUserRepository(r repository.UserRepository, c cache.Cache) repository.UserRepository {
    return &cachedUserRepository{repo: r, cache: c}
}

func (c *cachedUserRepository) cacheKeyAll() string { 
    return "users:all" 
}

func (c *cachedUserRepository) FindAll(_ []*model.User) ([]*model.User, error) {
    key := c.cacheKeyAll()
    if data, ok := c.cache.Get(key); ok {
        // Cache hit – deserialize.
        var users []*model.User
        if err := json.Unmarshal(data.([]byte), &users); err == nil {
            return users, nil
        }
        // If unmarshalling fails, fall back to DB.
    }

    // Cache miss – fetch from DB.
    users, err := c.repo.FindAll(nil)
    if err != nil {
        return nil, err
    }
    // Store result in cache (5‑minute TTL).
    if payload, err := json.Marshal(users); err == nil {
        c.cache.Set(key, payload, 300)
    }
    return users, nil
}

func (c *cachedUserRepository) Create(u *model.User) (*model.User, error) {
    created, err := c.repo.Create(u)
    if err == nil {
        // Invalidate the "list all" cache because data changed.
        c.cache.Delete(c.cacheKeyAll())
    }
    return created, err
}

```

### Cache Invalidation Strategy

When data mutates through `Create`, the decorator immediately calls `c.cache.Delete(c.cacheKeyAll())` to invalidate the list cache. This ensures eventual consistency—subsequent reads will miss the cache and fetch fresh data from MySQL. You can extend this pattern to individual entity caches by generating keys based on record IDs and invalidating them during update or delete operations.

## Wiring the Dependencies in Main

The final step injects the decorated repository into the use-case layer inside [`cmd/app/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/app/main.go). You instantiate the concrete GORM repository, wrap it with the cache decorator, then pass the wrapped version to `NewUserUsecase`.

```go
package main

import (
    "time"

    "golang-clean-architecture/pkg/adapter/repository"
    "golang-clean-architecture/pkg/cache"
    "golang-clean-architecture/pkg/config"
    "golang-clean-architecture/pkg/infrastructure/datastore"
    "golang-clean-architecture/pkg/infrastructure/router"
    "golang-clean-architecture/pkg/usecase"
    userepo "golang-clean-architecture/pkg/usecase/repository"
)

func main() {
    cfg := config.Load()
    db := datastore.NewDB(cfg) // returns *gorm.DB

    // Concrete repository
    userRepo := repository.NewUserRepository(db)

    // Cache implementation
    memCache := cache.NewMemoryCache(10*time.Minute, 15*time.Minute)

    // Decorated repository with caching
    cachedRepo := repository.NewCachedUserRepository(userRepo, memCache)

    // Use‑case gets the cached repository
    userUC := usecase.NewUserUsecase(cachedRepo, datastore.NewDBRepository(db))

    // Router receives the use‑case
    r := router.NewRouter(userUC)
    r.Run()
}

```

Because `NewUserUsecase` expects the interface `userepo.UserRepository`, the decorated `cachedRepo` satisfies the contract transparently.

## Performance Benefits and Trade-offs

Implementing this pattern delivers measurable performance improvements while preserving architectural integrity:

- **Separation of Concerns**: Cache logic lives in `pkg/cache` and [`pkg/adapter/repository/user_cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user_cache.go), keeping domain and use-case code pure.
- **Testability**: You can mock the `UserRepository` interface in unit tests or test the decorator independently by injecting a mock cache.
- **Swappable Implementations**: Replace `memoryCache` with a Redis-backed implementation by changing only the `cache.NewMemoryCache` call in [`main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/main.go).
- **Reduced Database Load**: Subsequent calls to `FindAll` hit the in-memory store, eliminating redundant GORM queries and MySQL round-trips.
- **Eventual Consistency**: Cache invalidation on write operations ensures data freshness without requiring distributed transaction complexity.

## Summary

- **Define a cache contract** in [`pkg/cache/cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/cache/cache.go) with `Get`, `Set`, and `Delete` methods to abstract caching mechanics from business logic.
- **Implement the decorator pattern** in [`pkg/adapter/repository/user_cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/repository/user_cache.go) by wrapping the concrete `UserRepository` and checking the cache before database calls.
- **Use JSON serialization** to store complex objects in the cache, handling cache hits by unmarshaling and misses by fetching from GORM then caching the result.
- **Invalidate caches** on write operations like `Create` to maintain data consistency between the cache and the MySQL database.
- **Wire dependencies** in [`cmd/app/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/app/main.go) by passing the decorated repository to `NewUserUsecase`, allowing the use-case layer to remain unaware of caching implementation details.

## Frequently Asked Questions

### Where should the cache logic reside in clean architecture?

The cache implementation belongs in the infrastructure or adapter layer, never in the domain or use-case layers. In the `golang-clean-architecture` repository, place the cache contract in `pkg/cache` and the repository decorator in `pkg/adapter/repository`. This maintains the dependency rule: inner layers (domain/use-case) depend only on interfaces, while outer layers (adapters/infrastructure) provide concrete implementations including caching.

### How do I handle cache invalidation when data changes?

Implement invalidation logic inside the decorator's write methods. In [`user_cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/user_cache.go), the `Create` method calls `c.cache.Delete(c.cacheKeyAll())` immediately after a successful database insert. This removes the stale "list all" entry, forcing the next read to fetch fresh data. For individual records, generate cache keys based on entity IDs and delete them during update or delete operations.

### Can I replace the in-memory cache with Redis or Memcached?

Yes. The `Cache` interface in [`pkg/cache/cache.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/cache/cache.go) abstracts the storage mechanism, so you can implement a `redisCache` struct that uses `go-redis` or similar clients without modifying the decorator or use-case code. Simply implement `Get`, `Set`, and `Delete` using Redis commands, then swap the implementation in [`main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/main.go) by replacing `cache.NewMemoryCache` with your Redis constructor.

### How does this pattern affect unit testing?

The decorator pattern preserves testability because the `UserUsecase` still depends on the `UserRepository` interface. In tests, you can inject a mock repository directly into the use-case to test business logic, or test the decorator separately by mocking the underlying repository and the cache interface. The cache implementation itself can be tested with a real in-memory instance or a stub to verify TTL and invalidation behavior.