April 27, 2018·4 min read·engineeringvalkeyrediscachingarchitecture

FreeWheel moves shared cache state out of application servers

FreeWheel describes replacing isolated application caches with shared Redis storage. The architectural gain is a common cache entry, while freshness still depends on the application policy.

A cache inside an application server has a limited scope of agreement. Another server can hold a different copy of the same logical value, and another application can follow a different refresh policy. Moving that value into Redis changes where applications meet to read and update it. The resulting architecture still needs a definition of when the cached value is valid.

In an April 27 guest post, FreeWheel technical architect Daniel Jones describes this transition. FreeWheel had used per-server, per-application caches with IIS and .NET applications. Sharing data across applications and server pools exposed stale-cache problems, while custom coordination became difficult to maintain and scale. The company adopted Redis and reports using Redis Enterprise in production on bare metal and in the cloud. Jones also identifies client-library support and integration with existing health monitoring as practical benefits.

I have used Redis since its initial release, and shared state with explicit commands remains one of its attractions. This account interests me because it identifies the coordination problem that makes an external cache useful. A cache can justify another service when it removes competing local copies that the application otherwise has to reconcile.

Shared storage changes the consistency problem

The architectural interpretation here is narrower than a claim that Redis automatically makes application data fresh. If two services address the same Redis key on the same authoritative server, they can use a common stored value. That removes one cause of disagreement, independently refreshed process-local copies. It does not define the relationship between that value and the database or service that originally produced it.

For example, consider a hypothetical cached product description. One service reads the underlying database and stores a serialized result. Another service reads the cache. The second service now depends on the first service to choose a suitable key, serialization format, and validity period. Sharing the location creates a shared contract between those services.

This example is an illustration, not a description of FreeWheel internals. The adoption account does not provide a key schema or an invalidation algorithm. Its value is the reported reason for changing the architecture. The mechanics of a particular implementation must be understood separately from a customer report that it worked for its author.

The Redis 4.0.9 string-command implementation, available before this account, makes several of those mechanics precise. SET accepts an expiration as part of the command. The implementation stores the value and attaches the requested expiry before returning success. Applications can therefore publish a value together with its lifetime through one operation.

SET catalog:product:42 "description-v1" EX 60
GET catalog:product:42

The command uses a hypothetical key and value. Its expiration argument means seconds, as the parser and generic setter show. The example demonstrates a bounded lifetime in the cache, without claiming that sixty seconds is an appropriate freshness target for any real workload.

Expiration measures time in Redis

The generic setter converts the supplied duration into a deadline using the server time. It rejects a nonpositive expiration argument. This is useful detail because the validity mechanism is a server-side deadline, not a promise that the original record has remained unchanged during that interval.

Suppose an application reads a source value, then pauses before storing it. The expiry starts when Redis processes the write. Time spent between reading the source and sending the cache write does not count against that requested lifetime. This follows from the source code adding the supplied duration to the current server time at the point it sets the expiry.

A source update can also occur while a cache entry remains within its lifetime. The cache may then contain exactly the bytes that the application wrote, with an unexpired deadline, while those bytes no longer describe the source. Choosing expiration is therefore a policy about acceptable reuse. It does not establish transactional agreement between separate systems.

The database expiration implementation reinforces that distinction. expireIfNeeded compares the stored deadline with the current time and removes expired keys on the primary. It has no application-specific knowledge about a product record or the revision of an external database row. The server can enforce the deadline it has, while the application must decide whether that deadline represents its actual freshness requirement.

I like that division when the application states the policy plainly. A key with a defined lifetime has understandable behavior. A shared cache described only as holding the correct data leaves unanswered which writer establishes correctness and how readers recognize a newer representation.

Conditional writes have a limited scope

The same string-command source supports NX and XX. NX makes a write conditional on the key being absent, while XX requires it to exist. Redis checks those conditions before setting the value and expiration. A command that fails the condition returns a null response instead of the ordinary success reply.

SET catalog:product:42 "description-v1" NX EX 60

This command can keep one cache fill from overwriting a value that already exists. It does not compare application revisions, and it does not check whether the supplied description is newer than the existing one. Those distinctions matter when several services share responsibility for a cache. A key-existence condition solves an existence problem, while ordering source revisions requires additional application information.

The source also accepts PX for millisecond durations. Greater timer precision does not strengthen the relationship between the cache and the original data. A precisely timed stale value is still stale according to the source system. The main design question remains what events cause replacement or invalidation and which service owns those operations.

An external cache consequently changes both deployment dependencies and application responsibilities. The FreeWheel account provides a concrete reason to accept that trade, coordination across applications had become difficult. Redis supplies operations for shared values and their lifetimes. The consuming applications still need compatible meanings for the keys and a policy connecting those lifetimes to the data they represent.