09 December, 2025

Should Microservices Only Talk Through APIs? Or Is It Okay to Read Data Directly?

 

If you’ve worked with microservices long enough, you know this debate never dies:

“Should every interaction between services go through APIs?
Or is it fine if one service reads another service’s data directly—as long as writes go through APIs?”

Some engineers treat API-only communication as a religion.
Others quietly open a read-replica and get on with their day.

The truth?
Like most architectural decisions, the answer is “it depends.”
But there is a sensible middle ground that successful companies naturally converge toward.

Let’s break it down.


The Ideal World vs. The Real World

Textbook microservices say this:

“Each service owns its data. No one touches its database. Everything goes through APIs.”

That’s beautiful… but reality is messier.

Real systems deal with:

  • High traffic

  • Low latency expectations

  • Cross-service reporting

  • Aggressive SLAs

  • Event-driven workflows

  • Services that evolve at different speeds

And sometimes, calling an API for every tiny read simply isn’t practical.

But allowing everyone to poke around in each other’s databases?
That’s a recipe for pain.

So how do we balance purity with practicality?


Let’s Start With the Easy Rule: Writes Must Go Through APIs

This one is non-negotiable.

Writes carry:

  • Business rules

  • Validation

  • Authorization

  • Side effects

  • Domain events

If one service writes directly into another service’s database, it’s basically bypassing the “brain” of that system.

That’s how data gets corrupted.
That’s how invariants break.
That’s how midnight outages are born.

So we’re aligned here:

Writes → Always through the service’s API

No shortcuts. No exceptions.


Reads, On the Other Hand… Are More Flexible

This is where nuance comes in.

Reads are often:

  • High volume

  • Latency-sensitive

  • Aggregation-heavy

  • Used for analytics or dashboards, not transactional logic

And hitting a service API for each read can create:

  • Extra hops

  • Failure chains

  • Scaling bottlenecks

  • Increased infrastructure cost

So it’s not surprising that many mature architectures start doing this:

**Use APIs for writes.

Use optimized data sources for reads.**

Yes — that means direct reads can be okay, but only if they’re done safely.

Let’s talk about what “safe” means.


When Direct Reads Are Safe

Direct reads don’t mean “connect to the main production database and hope nothing breaks.”

It means using a controlled, read-only source like:

1. Read Replicas

A service might read from a replica of another service’s database, isolated from writes.

2. CDC (Change Data Capture) Pipelines

Using tools like Debezium, Kafka Connect, Dynamo Streams, BigQuery Streaming, or Spanner Change Streams, a service can build a local read model of another service’s data.

3. Search and Analytics Indices

Elasticsearch, Redis, or BigQuery tables built specifically for reading.

4. Materialized Views

A “snapshot” of the data that’s updated asynchronously.

In all of these cases:

  • The write service remains the source of truth

  • No one is corrupting data

  • Schema evolution can be managed

  • Performance is optimized

This pattern is basically CQRS, but applied across microservices.


When Reads Should Not Be Direct

There are clear cases where you must use APIs and nothing else:

If the read involves business rules

Pricing, eligibility, discount logic — these belong inside the service.

If strong consistency matters

If “inventory = 1 item left,” a stale read can oversell.

If the schema changes frequently

Direct consumers break easily.

If security or PII restrictions apply

You don't want raw access leaking boundary controls.

In such cases, the API is the safe, stable contract.


The Architecture Most Companies End Up With

After scaling pains, outages, and refactors, most engineering orgs land here:

Writes → Always through the service’s API

Reads → Through a read-optimized, contract-driven data layer

(CDC, replicas, search indices, event streams, or materialized views)

This gives you the best of both worlds:

  • Services remain loosely coupled

  • Performance improves

  • You avoid API call chains

  • Schema changes don’t explode downstream systems

  • Traffic patterns become predictable

  • Teams can work independently

This approach isn’t about breaking microservice purity.
It’s about practical, scalable system design.


A Simple Example: Orders vs Inventory

If Inventory needs to check Orders frequently:

Bad: Inventory directly queries Orders’ main database

→ Tight coupling, fragile schema, risk of corruption

Good: Inventory gets a CDC stream or read replica of Orders

→ Local read model, fast queries, no business rule leakage

Writes?

Inventory can update Orders only through Orders’ API.
No exceptions.

Clean. Safe. Scalable.


So What’s the Final Answer?

Here’s the human, experience-based conclusion:

**✔ If you’re writing: use APIs.

✔ If you’re reading: choose whatever gives you performance and stability — as long as it’s read-only and contract-driven.**

This is how most high-scale systems operate behind the scenes.
It’s not dogma — it’s pragmatism.

Microservices aren’t about enforcing purity.
They’re about enabling teams to move fast without breaking each other.

And sometimes, that means being flexible about reads while staying strict about writes.

05 May, 2025

 

🧠 Neo4j Quirks: What You Should Know Before Going Graph

Intro

Neo4j is a powerful graph database, but like any tool, it comes with its own set of quirks—things that can trip up even experienced developers if they're expecting traditional RDBMS or NoSQL behavior. This post summarizes key “gotchas” you should be aware of when adopting Neo4j in real-world applications.


1. Relationship Directions Matter (Even When You Think They Don’t)

Neo4j relationships are directed:
(a)-[:FRIENDS_WITH]->(b) is not the same as (b)-[:FRIENDS_WITH]->(a)

Quirk:
When querying, you must match the direction unless explicitly saying otherwise:

MATCH (a)-[:FRIENDS_WITH]-(b) // direction-agnostic

Real-World Impact:
Queries may silently return nothing if you match the wrong direction.


2. Large Fanouts Can Kill Performance

What It Means:
A single node with thousands (or millions) of outgoing relationships becomes a performance bottleneck.

Quirk:
Even with indexes, traversals involving large fanouts are slow unless carefully limited.

Tip:
Avoid "celebrity nodes" or batch them with pagination:

MATCH (u:User)-[:FOLLOWS]->(f:User) WHERE u.name = "Ganapathy" RETURN f SKIP 0 LIMIT 100

3. Indexing Is Not as Granular as SQL

Quirk:
Neo4j indexes work only on node properties, not relationship properties or full-path matches.

Example:
You can't index a path like (a)-[:KNOWS]->(b).

Tip:
Use composite indexes or materialize paths into nodes if needed.


4. Orphaned Relationships Are Impossible by Design

Quirk:
You can’t create a relationship without linking it to two nodes.

Good News:
This enforces graph integrity.

Bad News:
You need to clean up nodes and relationships together:


MATCH (a)-[r]->() DELETE r, a

5. Aggregations and COLLECT Can Confuse New Users

Example:

MATCH (p:Person)-[:KNOWS]->(f) RETURN p.name, COLLECT(f.name)

Quirk:
The COLLECT() function creates lists, but mixing it with non-grouped values can lead to errors or misinterpretation.

Tip:
Always think in terms of how the result should group—Neo4j isn’t a traditional GROUP BY system.


6. No Strong Schema Enforcement

Quirk:
Neo4j is schema-optional—labels and property types are not strictly enforced.

Consequence:
You can accidentally insert inconsistent data (e.g., age as string vs number).

Mitigation:
Use CONSTRAINT and ASSERT where possible:

CREATE CONSTRAINT ON (p:Person) ASSERT p.id IS UNIQUE

7. Transactions Can Be Subtle in Cypher

Quirk:
Using the browser or driver may hide transactional nuances.

Gotcha Example:
Running CREATE in one query and expecting it to exist in a separate query in the same session may fail if autocommit isn't used.

Fix:
Wrap related operations in a transaction when using the driver:

tx := session.BeginTransaction() tx.Run(...) // commit explicitly

8. Testing and Cleanup Require Thoughtful Design

Quirk:
Neo4j's test environments don't reset easily unless you clean manually.

Tip:
Create a :TestRun node that relates to all temporary data, and clean it up:


MATCH (:TestRun)-[*]->(n) DETACH DELETE n

✅ Final Thoughts

Neo4j is expressive and great for certain data models—but it’s not magic. Understanding its quirks early helps avoid performance and correctness pitfalls.

Recommended Next Steps:

26 January, 2025

Understanding Read Your Own Writes Consistency in Distributed Systems

Have you ever updated your social media status only to refresh the page and find your update missing? Or modified a product listing that seemingly disappeared into the void? These frustrating user experiences often stem from a crucial concept in distributed systems: Read Your Own Writes (RYW) consistency.

Why RYW Consistency Matters

In my recent DZone article, I dive deep into the world of Read Your Own Writes consistency - a critical yet often overlooked aspect of distributed system design. While technical requirements often focus on scalability and performance, RYW consistency directly impacts how users perceive and interact with our systems.

Think about it: when you make a change to any system, you expect to see that change immediately. It's not just a technical preference; it's a fundamental user expectation that shapes trust in our applications.

Key Challenges in RYW Implementation

Implementing RYW consistency isn't as straightforward as it might seem. In my DZone article, I explore several key challenges:

  • Managing complex caching layers across browsers, CDNs, and application servers
  • Handling load balancing while maintaining consistency
  • Dealing with replication lag in distributed databases

These challenges become particularly evident in real-world scenarios like social media updates, collaborative document editing, and e-commerce inventory management.

Implementation Strategies That Work

The article outlines several practical implementation strategies, including:

  • Sticky sessions for consistent request routing
  • Write-through caching for immediate updates
  • Version tracking to ensure consistency

Each approach comes with its own trade-offs and considerations, which I discuss in detail in the full article.

Want to Learn More?

If you're interested in diving deeper into RYW consistency and learning about specific implementation strategies, check out my complete article on DZone

The article includes code examples, best practices, and monitoring strategies to help you implement RYW consistency effectively in your distributed systems.

Final Thoughts

As distributed systems become increasingly complex, understanding and implementing RYW consistency becomes more crucial than ever. While eventual consistency might be acceptable for some use cases, users expect their own changes to be reflected immediately.

Have you encountered RYW consistency challenges in your systems? I'd love to hear about your experiences in the comments below.


This post is a companion piece to my detailed technical article on DZone about Read Your Own Writes consistency. For implementation details and code examples, please refer to the full article.