This is Part 3 of "The Muse of Microservices," a series on the system design behind Polyhymnia. Part 2 covered the Go gateway that calls both services you're about to meet.
Two backend services, two philosophies: how Polyhymnia's Rust database owner protects a single SQLite file, and how its C++ engine turns picking one array index into a systems-programming set piece.
Two services live behind the Go gateway, and they couldn't have more different personalities. One holds the only piece of mutable state in the entire system and treats that responsibility like a vault. The other holds nothing at all and treats a coin flip like a research paper. Put side by side, they're a compact lesson in two of the most fundamental axes of service design: who owns data, and what it costs to keep a service stateless.
Rust: the database's only bodyguard
The Rust service's full title in the README is "Database Manager & Safety Layer," and both halves of that name are backed by real code, not vibes.
One owner, no exceptions
Exactly one service in Polyhymnia is allowed to open quotes.db. Not the Go gateway, not the C++ engine — only Rust, and every other service reaches the data exclusively through two gRPC methods: GetAllIds and GetQuoteById. This is the database-per-service principle in its purest, smallest possible form: shared databases create a hidden coupling between every service that touches them, because a schema change in one now has to be coordinated with every other reader and writer, whether or not they ever talk to each other directly. Give exactly one service exclusive ownership of the data, and that coupling problem simply doesn't exist. Everyone else negotiates through an API instead of a schema.
Parameters, not string concatenation
The "safety" in the service's name isn't just branding — it shows up directly in how every query is built:
conn.query_row(
"SELECT quote, author FROM quotes WHERE id = ?1",
params![id],
|row| { /* ... */ },
)
That ?1 placeholder, filled in via rusqlite::params!, is a parameterized query — the value is bound separately from the SQL text rather than spliced into a string. It's a one-line habit that closes off SQL injection as a category entirely, because the database driver never has to guess where a query ends and untrusted data begins. Every query in the service follows this pattern; there isn't a single interpolated string anywhere in the SQL layer. For a service whose entire reason for existing is to be the trustworthy custodian of the one file everyone else depends on, that's not a stylistic footnote — it's the whole job description.
The concurrency model, and its ceiling
The connection itself is wrapped like this:
struct QuoteDbService {
conn: Arc<Mutex<Connection>>,
}
One SQLite connection, shared across every request, guarded by a tokio::sync::Mutex. In practice, that means every database operation the service performs — every GetAllIds, every GetQuoteById — is serialized. Only one query runs at a time, full stop, no matter how many requests arrive concurrently.
That's a completely reasonable choice for this project, and it's worth understanding why it's reasonable rather than just accepting it. Arc<Mutex<T>> is the simplest possible way to share mutable state safely across async tasks in Rust — it trades throughput for correctness and simplicity, which is exactly the right trade when your query load is "occasionally, one at a time, from a demo app." It would become the ceiling on this service's scalability the moment real concurrent load showed up — a connection pool (handing out several connections instead of guarding one) is the natural next step if that ever became a problem. Knowing which lever to pull, and when it's actually worth pulling, is most of what capacity planning is.
Interestingly, the service still turns on SQLite's write-ahead log:
let _: String = conn.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?;
Given that the app itself already serializes every query through the mutex, that might look redundant — and for traffic that arrives only through this service, it is. But WAL mode also changes how SQLite behaves when something outside the app touches the same file — a sqlite3 CLI session opened for debugging, for instance, which the project's own troubleshooting docs describe doing. WAL lets readers avoid blocking on a writer at the file level, not just at the application level, which is a nice bit of defense-in-depth for a database file that's meant to have exactly one legitimate writer but might occasionally get an uninvited guest.
Idempotent bootstrapping
On first launch, the service creates its own schema and seeds itself with ten quotes — but only if the table is empty:
let count: i64 = conn.query_row("SELECT COUNT(*) FROM quotes", [], |row| row.get(0))?;
if count == 0 {
/* seed */
}
CREATE TABLE IF NOT EXISTS plus a count-gated seed step means the service can be started, killed, and restarted indefinitely without ever duplicating data or crashing on an already-initialized database. It's a self-contained, self-healing bootstrap — a tiny version of what a real migrations framework buys you at scale, running here with nothing but a conditional and a transaction.
C++: a coin flip with a PhD's worth of ceremony
Then there's the Randomizer. Its interface is one RPC — SelectRandomId(IdList) -> SelectedId — and its job, stated as plainly as possible, is: given a list of numbers, return one of them.
Here's how it actually does that:
std::random_device rd;
const uint64_t hi = static_cast<uint64_t>(rd());
const uint64_t lo = static_cast<uint64_t>(rd());
const uint64_t seed = (hi << 32) | lo;
std::mt19937_64 engine(seed);
std::uniform_int_distribution<std::size_t> dist(0, count - 1);
Two independent pulls from the OS's entropy source, bit-shifted together into a single 64-bit seed, fed into a Mersenne Twister engine, which is then sampled through a uniform distribution to get an index. And then, instead of just calling .at(index) like a person in a hurry:
const int64_t* base = request->ids().data();
const int64_t* target = base + index;
const int64_t selected_id = *target;
...it walks a raw pointer to the target element by hand. This is, transparently, a joke — the README says as much, and one call to dist(engine) alone would have been more than sufficient to pick a number out of a list of a dozen IDs. But the ceremony is worth taking seriously for a second, because every individual piece of it is a real pattern lifted from a context where it genuinely matters:
-
random_deviceseeding a fast PRNG is exactly how you'd build a cryptographically-aware random number generator: pull true entropy once, expensively, from the OS, then use it to seed a much faster deterministic generator for everything after. It's the right shape, applied to a problem that never needed it. - One footnote worth knowing regardless: the C++ standard never actually guarantees
std::random_deviceis non-deterministic. Some standard library implementations fall back to a deterministic PRNG when no hardware entropy source is available on the platform. It's the kind of assumption that's safe 99% of the time and exactly the kind of thing worth double-checking before you build something security-sensitive on top of it. - The pointer arithmetic is functionally identical to
operator[]— the compiler will very likely generate the same instructions either way — but it's a nice reminder that in C++, "correct" and "idiomatic" are two different bars, and this project is having fun clearing the first one while cheerfully ignoring the second.
Stateless, and proud of it
The one thing the Randomizer is genuinely, unironically good architecture for: it holds no state whatsoever. It doesn't know what a quote is, doesn't know what a database is, and doesn't remember anything between calls. Its only input is a list of integers; its only output is one of them.
That statelessness is the real payoff, and it's worth contrasting directly against the Rust service sitting right next to it. Because the Randomizer carries no data between requests, you could run ten copies of it behind a load balancer tomorrow with zero coordination between them — no shared cache to invalidate, no lock to contend over, nothing to keep in sync. The Rust service, by contrast, can't be casually replicated at all without first solving the much harder problem of who owns the one SQLite file. Two services, two completely different scaling stories, sitting three lines of Go apart in the gateway's handler. That contrast — stateless services scale by copying, stateful services scale by coordinating — is one of the most load-bearing ideas in distributed systems, and Polyhymnia hands it to you as a two-service case study you can read in an afternoon.
It also validates its own input, which is easy to overlook: an empty ID list returns INVALID_ARGUMENT rather than crashing or silently returning garbage. Small, boring, and exactly what you want from a service sitting at a trust boundary.
What's next
We've now met all four services and the one contract that lets them talk to each other. Part 4 zooms out to that contract itself — one .proto file feeding three independent codegen pipelines — and follows the project the rest of the way to a running container stack, including a networking subtlety in the Docker setup that's easy to miss until you go looking for it.
Top comments (0)