Back to Blog
•
ByteGurus

Why our platform services never call each other

How MicroFrame services exchange state through events and local projections instead of gRPC calls, and what we measured before making it a rule.

Architecture Event-Driven Kafka MicroFrame

The services of our MicroFrame platform do not call each other. No gRPC between them, no REST, no “quick lookup” over the mesh. When one service needs to know something another service owns, it keeps its own copy, fed by events. The one exception is deliberate and signed: the API gateway streams its license snapshot from the license service. We wrote this down as a rule on 12 September 2026, after a day that showed what synchronous calls had been costing us.

The day that made it a rule

We had spent most of that day building service-to-service trust: credentials for machine callers, delegation, the plumbing that lets service A prove to service B who it is acting for. All of it existed to support three calls. A deployment service asked the policy service whether an action was allowed. Two content services asked the CMS for knowledge-base articles.

When we looked at those three calls closely, none of them needed to be synchronous. One was worse than unnecessary: the deployment service called a policy method that the policy service had never exposed. Every call came back Unimplemented, every deploy was refused, and the error was easy to read as “permission denied” rather than “this method does not exist”.

We also counted. Most core services already made zero outbound calls to other services. The infrastructure we were building served a handful of call sites, and each of those call sites had an event-shaped answer.

What replaces a call

There are three kinds of question a service asks another, and each has an event-shaped answer.

“What is the current state of X?” The owner publishes state changes; the consumer keeps a local projection and reads it like any other table. A read never leaves the service, so the owner being slow or down does not make the reader slow or down.

“Please do X.” The requester publishes XRequested; the owner does the work and publishes XCreated with the same correlation id. The requester learns the outcome from the event, not from a response it is blocked on.

“Is this caller allowed to do X?” Every service runs its own policy enforcement point against a local copy of the permissions that apply to it. Policy decisions are made where the request lands, not by asking the policy service over the network.

How events leave a service

Every service writes its events to an outbox table in the same database transaction as the change that produced them, and a relay moves them to Kafka. If the transaction rolls back, the event never existed. If Kafka is down, the event waits in the table.

One lesson cost us more than it should have: an event type that was not registered with the outbox was silently dropped. The row stayed in the table, the relay never picked it up, and the consumer simply never heard about the change. There was no error anywhere. Registering every event type eagerly at startup is now part of how a service is built, not something remembered per event.

Projections that can be rebuilt

For state that consumers need in full (who belongs to which tenant, which permissions a role grants), the owner publishes to a compacted topic keyed by the entity. Kafka keeps the latest record per key, and a deletion is a tombstone.

That gives the consumer a useful property: it can rebuild its whole projection by reading the topic from the start. Our API gateway does exactly that. Each gateway pod reads the topics with its own consumer group, replays them from the beginning on startup, and answers 503 until its projection is seeded. A fresh pod never serves an answer from half-loaded data.

What it costs

A projection is eventually consistent. A permission granted a moment ago may not be visible to every service yet, and screens have to be honest about that. We accept this because the alternative was worse: a chain of synchronous calls where one slow service makes every caller slow, and where a missing method looks like a denied request.

It also moves work earlier. A new feature that needs another service’s data starts with the event and the projection, before any screen is built. That is slower on day one. It has been faster every day after.

The rule, as we use it

Before a platform service adds a client to another platform service, we write down the event that would carry the same information. Every call site we have reviewed since September had one.

B

ByteGurus

Author

Back to all articles
Ready to Start?

Ready to start your project?

Let's discuss how we can help bring your ideas to life.