Upsert combines insert and update for a key-value cache. Give it a key and value: if the key is absent, the cache adds it; if present, the cache silently replaces its older value. A subsequent Read finds the specified value. This policy suits a cache where newer values are usually more useful, but it does not suit every storage operation. section 7.1.3
Listing 7.4 cache.go: Implementing the Upsert function on the cache
// Upsert overrides the value for a given key.
func (c *Cache[K, V]) Upsert(key K, value V) error {
c.data[key] = value
// Do not return an error for the moment,
// but it can happen in the near future.
return nil
}
The initial Go implementation assigns directly to the map. Its error return is reserved for possible future failures, such as a capacity limit; this version always returns nil. section 7.1.3
A size-limited cache needs more than map assignment. It tracks keys in insertion order and treats an overwrite as a fresh insertion. Under a mutex, Upsert removes an existing key from that order; for a new key at capacity, it removes the oldest entry instead. It then adds the value and key together. The helper also sets the entry’s expiration time, so an overwrite refreshes its TTL. This later version has no error return, unlike the initial method. section 7.4.2 section 7.1.3
Listing 7.20 cache.go: Upsert with the new helper functions
func (c *Cache[K, V]) Upsert(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
_, alreadyPresent := c.data[key]
switch {
case alreadyPresent:
c.deleteKeyValue(key) #1
case len(c.data) == c.maxSize: #2
c.deleteKeyValue(c.chronologicalKeys[0])
}
c.addKeyValue(key, value) #3
}
Follow the two branches before the final add: an existing key is requeued, while a new key evicts the oldest only when the cache is full. section 7.4.2
Check the eviction order
With capacity three, Upsert keys 1, 2, and 3. Upsert key 1 again with a new value, then Upsert key 4. Key 2—not key 1—is now the oldest and is discarded. The capacity test checks this behavior with Read. section 7.4.2
Choose the operation name to match the storage rule. A game repository instead separates Add from Update: Add rejects an existing game, and Update is intended to reject an absent one. A single Upsert would erase that distinction. section 8.4.2
