← All insightsArchitecture

Microservices without per-service data scaffolding

Every new service normally brings its own data access code, its own cache and its own invalidation bugs. One shared API in front of the data removes that scaffolding for small teams.

4 min read

Ask a team with a dozen services what it costs to add a thirteenth, and the answer is rarely about the business logic. It is about the scaffolding: a data access layer to write, a cache to stand up, invalidation rules to get right, a web interface to expose, page sizes and error formats to agree. Each is a solved problem, solved again, slightly differently, by every team, every time.

What a new service carries

Take a service that owns nothing new and simply reads customer and order data already in the shared database. To do that well, someone writes:

  • data access code: queries, mapping rows to objects, connection handling
  • a cache in front of those queries, because the read rate will not fit on the database
  • invalidation logic, so the cache stops serving stale rows after a write elsewhere
  • a web or gRPC interface with consistent paging, filtering and error formats
  • tests for all of the above, plus monitoring for the cache in particular

None of that is the feature. All of it is required before the feature can ship. And because each service does it separately, quality varies by who wrote it and when.

The invalidation piece is the one that bites. A write in the order service must somehow reach the cache in the reporting service, so teams add events, message queues and code that clears the cache after every write, which drift over time. When they drift, the symptom is a customer seeing yesterday's data, and the investigation crosses three teams.

One API in front of the data

The alternative is to stop building that layer per service and build it once, driven by the database schema (the layout of its tables). An API sits in front of the database and exposes typed entry points for every table: Get, Insert, Update, Delete, GetRows and GetTotalRowCount, over REST and gRPC (two standard ways for programs to call a service over a network), with the same paging, filtering and error formats everywhere. Every service reads through it.

Behind the API, reads are served from Redis (a memory store that answers in about a millisecond) and writes go straight to the database, which stays the source of truth. A sync service watches the database for changes using the mechanism suited to the engine, such as change tracking on SQL Server read on a short interval, and carries every change into Redis. Invalidation is no longer something any service implements; the cache is correct because the sync keeps it correct.

Joins and multi-field queries are served from Redis too, not only lookups of one row by its identifier. Most useful reads are "orders for this customer in this status", not "order by id"; without joins in the cache, teams fall back to the database for anything beyond a simple lookup and the cache stops earning its keep.

A new service should cost the effort of its own logic, not the effort of rebuilding the data layer around it.

What a team under twenty engineers gains

The gain is concentrated in small teams because they have no platform group to absorb the scaffolding. In a team of twelve, the engineer who writes the cache is also the engineer who ships the feature, and every quarter spent on plumbing is a quarter of product not shipped.

  1. Adding a service becomes a matter of wiring its logic to entry points that already exist. No data access layer, no cache, no invalidation.
  2. Every service behaves the same way at the data boundary, so an engineer moving between services does not relearn a custom data layer.
  3. Cache correctness is one property of one system, not twelve properties of twelve systems written by twelve people.
  4. The database does far less read work, so the shared data layer stops being the reason services are slow.

A cache kept correct by the sync rather than by careful code review does not need a specialist watching it, and a data layer serving reads from memory does not need a database administrator tuning it every month. Generalists ship the work.

Where the pattern does not fit

It does not fit a service whose data is genuinely its own and never shared; keep that store private to the service.

It does not fit workloads dominated by writes. The API passes writes straight through to the database, so a service that takes in a constant flood of device readings gains little on the read side and still needs a database sized for its writes.

It does not fit reads that must reflect the very latest write. On SQL Server the cache is updated on a short interval, so a read may briefly return the previous value; a service that cannot tolerate that should read those few rows through the database. It does not remove the need for service boundaries and ownership; it removes the plumbing inside each one, not the design work between them.

The reasonable objection is that one API for every service becomes one thing to break. If Redis is unreachable, reads fall back to the database automatically, so the cache is a faster path that steps aside rather than a new point of failure; and the database was already the shared dependency underneath twelve separate caches.

Where redfly fits

redfly is the API and the sync service described here, run against your SQL Server database in your own cloud account, so a team under twenty engineers gets one data layer instead of building one per service. We put it in place with design partners against their existing tables.

Ready when you are

Stop reading. Start shipping.

Work with us as a design partner and see the difference on your own database.

redfly API + Sync Service · Licensed directly from redfly