August 6 2025
Internal Module Contracts Without Inventing RPC Inside the Same App
Separating modules should not force the team to fake network, versioning, and distributed protocol inside a monolith that is still one deployment.
Andrews Ribeiro
Founder & Engineer
3 min Intermediate Systems
The problem
When a team starts modularizing a backend, two bad extremes appear.
First extreme:
- any module imports anything
- queries cross contexts
- internal rules leak with no ceremony
Second extreme:
- every local call turns into a pseudo API
- every module gets a client, DTO, facade, and version
- the monolith starts pretending it is a mesh of microservices
Both extremes wear the codebase down.
The first because it is too loose.
The second because it is theatre.
Mental model
An internal contract is not the same thing as a distributed protocol.
An internal contract exists to answer:
- how should this module be used?
- what does it promise?
- what does it expect?
- what can change internally without breaking others?
Inside the same app, that can often be solved with:
- a small interface
- one clear entry point
- coherent internal types
- explicit semantics for errors and results
without pretending there is latency, versioning, and serialization where no network exists yet.
What is usually enough
In a modular monolith, it is often enough for another module to talk to you through:
- one public use case
- one small internal facade
- one well-named application service
- internal events when decoupling actually helps
The important part is having a recognizable entry point.
It is not making every local call look like HTTP.
Simple example
Imagine orders needing to ask something from billing.
One bad loose solution:
ordersimports abillingrepository- it reads billing tables directly
One bad theatrical solution:
billingexposes an internal pseudo client- serialized payload
- response envelope
- mapping as if it were a remote call
A healthier solution:
billingexposes one clear internal port, such ascheckChargeabilityorgetAccountStandingordersdepends on that semantic meaning, not on internal details
The contract exists.
But the team did not have to act out a network.
When an internal event helps
Sometimes the best contract is not a direct call at all.
If one module only needs to react to a fact, an internal event may be a better fit.
Examples:
- order confirmed
- user blocked
- subscription canceled
But the same rule still applies:
a useful internal event is not a generic substitute for every call.
If a module needs an immediate answer to decide now, then an event may not be the right shape.
The common mistake
The common mistake is importing the microservices discussion without looking at the real runtime.
If it is still:
- the same deploy
- the same process
- the same database
then the best internal contract may be much lighter.
Otherwise, you duplicate mental cost without gaining proportional isolation.
How a senior thinks
Engineers who choose better usually ask:
- does this module need an explicit boundary or a theatrical protocol?
- who is allowed to call this and through what entry point?
- which dependency am I trying to protect?
- am I preparing for future extraction with good judgment, or just paying the cost now?
That usually produces better boundaries and less theatre.
Interview angle
This topic appears in questions about modular monoliths, service boundaries, and architecture evolution.
The interviewer wants to see whether you:
- know how to create a real internal boundary
- avoid both free-for-all coupling and scenic abstraction
- understand that an internal contract is not a copy of a public API
A strong answer often sounds like this:
“I would create one explicit entry point per module, with clear internal semantics, but I would avoid simulating RPC inside the same app. The goal is to protect boundaries and reduce coupling, not to pretend distribution before it exists.”
Direct takeaway
A good internal contract makes a module more predictable.
It does not make a monolith perform at a microservices conference.
Quick summary
What to keep in your head
- An internal module needs a clear contract, but it does not need to simulate RPC just to look distributed.
- A good internal contract defines intent, dependency, and semantics. It does not automatically require HTTP, versioning, and serialization.
- If everything can call everything, modularization is decorative.
- If every local call requires integration theatre, the team bought complexity too early.
Practice checklist
Use this when you answer
- Can I say how other modules are supposed to use this one?
- Does my internal contract express business intent or only mirror implementation details?
- Am I forcing a network-style protocol where a clearer code boundary would be enough?
- If one module changes internally, do the others stay stable?
You finished this article
Share this page
Copy the link manually from the field below.