Subscribe to get high-signal insights on how modern fintech is built.

engineering

Your Own Services Are External

Edge-only validation protects against strangers but not against your own services passing bad data.

By Alex Kugell ·

Your payment service tells your ledger service that a $4,200 transfer just settled. The ledger books it, revenue goes up, and the daily reconciliation report looks clean.

Two days later, someone in operations pulls the bank's settlement file. The transfer is still pending. The bank never confirmed it. Your payment service reported a status it read from its own cache, not from the bank, and the ledger trusted the claim because it came from inside the building.

The obvious fix is better monitoring, tighter alerts, maybe a circuit breaker on the payment service. But the payment service worked correctly by its own logic. It returned the status it had.

The problem is that the ledger accepted an internal claim with the same blind faith it would never extend to an outside caller. This is a story about what happens when your own services trust each other the way a monolith trusts itself, even though they stopped being a monolith years ago.

The perimeter delusion

Most fintech teams draw a hard line at the API gateway. Everything outside gets validated, rate-limited, schema-checked, and logged, but everything inside gets a pass because "we control both sides."

Google's security engineering team identified this pattern. The assumption that internal services deserve implicit trust because they share a codebase or a Kubernetes cluster is, in practice, a perception problem. Their Building Secure and Reliable Systems handbook starts from the position that no service inherently trusts another.

The reasoning is straightforward. Your services communicate over a network, run on different machines, deploy at different times, and cache state independently. They're as external to each other as any third-party API, except they have broader permissions and less monitoring.

A monolith hides this. When your payment logic and your ledger logic live in the same process, a function call is a function call. The data is consistent because it's reading from the same memory.

Split those into services, and the function call becomes a network request. The data consistency guarantee disappears, but the trust assumption doesn't.

Four ways internal trust fails

These scenarios share a pattern: one service passes data that looks correct, the next service accepts it without checking, and the error compounds silently.

Unverified settlement status. The payment service marks a transaction as settled based on an optimistic state update. The ledger books revenue. But the bank's confirmation hasn't arrived yet, and when it does, the status is still pending.

The ledger now has a $4,200 entry backed by nothing. If the settlement eventually fails, the reversal is a manual process that touches the user's balance, the revenue report, and the reconciliation log.

Stale cached balance. The account service caches balances with a 30-second TTL. A customer's balance just dropped to $12 from a debit that cleared. The payment service asks the account service for the current balance, gets $1,200 from before the debit, and approves a $500 payment.

The payment service didn't make an error by its own logic. It asked for a balance, got one, and acted on it.

The account service didn't make an error either. It returned what it had. The gap between what it had and what was true is where the overdraft lives.

Inherited bad assertions. The KYC service calls an identity provider and gets a verification response. It doesn't validate the response schema or check that the fields it needs are actually populated, something I covered in my piece on the three axioms as the "no trust" rule for external data. The KYC service stamps the user as verified and passes that assertion downstream.

Now the onboarding service trusts the KYC service's stamp, the account service trusts the onboarding service, and a lending service trusts the account service. Four services deep, nobody has checked the original claim. If the identity provider returned a malformed response that the KYC service deserialized into defaults, every downstream decision is built on air.

Trust Cascade: One Bad Assertion, Four Inherited Decisions
Identity Provider
Returns malformed verification
external
KYC Service
Deserializes to defaults, stamps "verified"
Onboarding Service
Trusts KYC stamp, passes assertion
Account Service
Creates account based on assertion
Lending Service
Approves loan for unverified identity
trust boundary
no validation at any internal boundary
The original claim was never verified. Every service added authority, none added evidence.

Silent deployment breakage. Service A updates its response schema. Service B, which consumes that response, hasn't deployed the matching change yet. The new field arrives as null or gets deserialized into a default. Service B doesn't fail. It proceeds with a zero where there should be a fee amount, or a default status where there should be a specific state. The data looks structurally valid but isn't true.

Why internal failures are worse

An external API going down is noisy. Timeouts fire, alerts trigger, someone pages the on-call. But your own service returning bad data is silent.

Internal services typically have broader permissions than external integrations. Your payment service can write directly to the ledger. An external webhook goes through validation middleware first. The attack surface of a compromised or buggy internal service is larger, and the monitoring is thinner.

SOC 2 auditors understand this distinction. The Trust Services Criteria evaluate processing integrity regardless of where the data originated, internal or external. If your ledger accepts unvalidated claims from an internal service, an auditor sees the same control gap as if it accepted unvalidated claims from a webhook.

NIST SP 800-207 formalizes it further. Zero trust means no implicit trust granted based on network location or asset ownership. Google's BeyondProd framework translates this into production practice: mutual TLS between services, per-service identity, and policy enforcement at every boundary, not just the edge.

What boundary validation looks like

Verification at every service boundary is the fix, and it has four layers.

Schema contracts. Every service validates inbound payloads against a schema, even from internal callers. Protobuf, JSON Schema, Zod, whatever your stack uses. If a field that should be present is missing, the service rejects the request rather than falling back to defaults or proceeding with partial data.

Domain invariant checks. Structural validation catches missing fields, but domain validation catches wrong values.

When the payment service says a $4,200 transfer settled, the ledger doesn't just check that the amount field is a number. It checks that $4,200 is consistent with the transfer it has on record, that the transfer exists, that its current status allows a settlement transition, and that the settlement amount matches the original authorization.

Fail closed on the unexpected. An internal service returning an unexpected field, an unknown enum value, or a payload that doesn't match the contract should halt processing, not log a warning and continue or substitute a default. I covered in an earlier piece why proceeding with invented data is how balances drift.

Independent verification for high-value operations. For anything above a threshold, the consuming service confirms the claim through a second channel. The ledger pulls the transaction status from the bank's API before booking it.

Two sources, one answer. If they disagree, the operation blocks until a human or a reconciliation process resolves it.

This costs latency. An extra API call to the bank adds 200 to 400 milliseconds. For a $4,200 settlement that will sit on the ledger for years, that trade is obvious.

The restaurant kitchen, again

I used a restaurant kitchen in my three axioms piece to explain why you check the ticket, not the voice. That analogy was about strangers walking in off the street.

This is about the line cook who's worked there for ten years telling the head chef that table 12 ordered the lobster. The chef still checks the ticket.

Not because the line cook is untrustworthy, but because the line cook might be reading yesterday's prep list, the order might have changed after the cook last looked, or the cook might be relaying what the server said, who misheard the customer.

The cook's tenure doesn't change the verification step. The ticket is the source of truth. Everything else is a claim.

Sources

Alex Kugell
Alex KugellAI/ML Engineer, Fintech Infrastructure

Writes Ledger Drift. Engineer building AI systems and financial infrastructure. Open source contributor to x402, ACK, and MPP – agentic payment protocols.

Frequently Asked Questions

Why should internal microservices validate each other's data in financial systems?
Internal services can pass bad data for the same reasons external APIs can: stale caches, partial deployments, race conditions, and deserialization into defaults instead of failures. A payment service telling the ledger that a transaction settled, when the bank hasn't confirmed, causes early revenue booking that reconciliation catches days later. The blast radius is often larger than external failures because internal services have broader permissions and less monitoring.
What is the MSA trust perception problem?
Microservice architectures inherit an assumption from monolithic systems that everything behind the API gateway is trusted. Google's security engineering team calls this a perception problem because microservices communicate over a network, often across deployment boundaries and machine boundaries, which means they share the same failure modes as external calls: network partitions, version skew, and stale state. The code may be yours, but the runtime behavior is not.
How does a stale cache in one service cause an overdraft in another?
An account service caches balances with a 30-second TTL. A customer's balance drops to $12 from a debit that just cleared. The payment service asks the account service for the current balance, gets the cached $1,200 from before the debit, and approves a $500 transfer. The payment service did nothing wrong by its own logic. It asked an internal service for a balance and got an answer. The problem is that it treated that answer as a fact.
What does boundary validation look like between internal fintech services?
Four layers: schema validation on every inbound payload even from internal callers, domain-level invariant checks where the consuming service verifies claims against its own state, fail-closed behavior on unexpected or missing fields instead of proceeding with defaults, and independent verification for high-value operations where the consuming service confirms claims through a second channel rather than trusting the upstream service alone.
Does NIST zero trust architecture apply to internal service communication?
Yes. NIST SP 800-207 defines zero trust as continuous verification with no implicit trust granted based on network location or asset ownership. Google's BeyondProd framework applies this specifically to production microservices, replacing the perimeter model with per-service identity, mutual TLS, and policy enforcement at every service boundary. SOC 2 auditors increasingly expect evidence of internal service authentication and authorization, not just edge controls.

Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.

Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.

Keep reading

ai & mlAgentic AML Is ShippingThree companies shipped AI agents that run real AML investigations, and their production numbers don't measure the same thing.
analysisBlock Wants Two BanksBlock already runs an FDIC-insured bank, so its application for an uninsured trust bank is about which charter fits which asset class.
fintechHow BaaS Providers Actually WorkBaaS providers rent you a bank charter through an API, and the architecture of that rental determines who holds the risk.
View more ›