Skip to main content

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

Andrews Ribeiro

Founder & Engineer

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:

  • orders imports a billing repository
  • it reads billing tables directly

One bad theatrical solution:

  • billing exposes an internal pseudo client
  • serialized payload
  • response envelope
  • mapping as if it were a remote call

A healthier solution:

  • billing exposes one clear internal port, such as checkChargeability or getAccountStanding
  • orders depends 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

Practice checklist

Use this when you answer

You finished this article

Next article Blast-Shield Layers for Internal Spikes Without Taking Down the Core Previous article Internal Circuit Breakers Between Modules Without Becoming a Reflex for Any Slowness

Keep exploring

Related articles