Small teams often split systems too early because microservices sound mature. The harder engineering move is to keep a system simple until a boundary has earned its independence. A good boundary lowers coordination cost. A bad boundary creates a distributed monolith with more dashboards.
Start with the cost of change
I look for code that changes for the same reason. Billing rules change when pricing changes. Invitation rules change when trust and membership policies change. Search ranking changes when product discovery changes. If two areas change for unrelated reasons, a boundary may be forming. If they change together every sprint, splitting them will probably create friction.
Give data an owner
The clearest boundary is usually data ownership. If the billing module owns subscriptions, other modules should not casually write subscription rows. They should ask billing for the answer they need: is this organization active, what plan is it on, can this usage event be accepted?
// Better than leaking billing tables everywhere
const access = await billingPolicy.canUseFeature({
organizationId,
feature: "team_invites"
});
Prefer boring contracts
Boundaries should be boring. A function call in a modular monolith is fine. A REST endpoint is fine. An event is fine when consumers do not need synchronous answers. What matters is that the contract names the domain concept clearly and hides internal storage details.
The danger sign is a contract shaped like the database: getSubscriptionRowByOrgId. A better
contract is shaped like a decision: canCreateWorkspaceMember. The second one makes the caller
depend on policy, not schema.
Split deployment last
I like modular monoliths for small teams because they let the architecture communicate boundaries without forcing every boundary to become a network hop. Separate deployment should come when there is a concrete reason: independent scaling, fault isolation, security isolation, or team ownership that justifies the operational cost.
The practical rule: split code first, split data carefully, split deployment last. That sequence gives a team room to learn the domain before paying the full price of distributed systems.