Boundaries before services

A service split is easier to trust after the workflow boundary already makes sense.

Large codebases have a way of turning every architecture conversation into a microservices conversation. I understand the temptation. A big folder feels embarrassing, five boxes look tidy, and an arrow between OrderService and PaymentService gives the meeting something concrete to point at. The awkward part is that nobody has explained where an order actually stops and a payment begins. HTTP will happily carry that confusion into production and charge us for retries, timeouts, tracing, deploy order, and a very long Slack thread called quick ownership question.

I would still split services when teams need their own deploy schedules, billing has security rules that profile settings will never need, or one workload eats CPU while its neighbor mostly sits around looking pretty. Those reasons make sense in production. A codebase feeling large is usually a reason to understand it better first.

Five tidy boxes and one confused order#

Take an app with orders, payments, inventory, shipping, and email. The nouns are already begging to become services, so somebody draws this list on a diagram:

  • OrderService
  • PaymentService
  • InventoryService
  • ShippingService
  • EmailService

The diagram has excellent manners. Every noun stays in its box and the arrows go in one direction. Then a user places an order, payment succeeds, inventory times out, the confirmation email goes out anyway, and shipping consumes an event it was not quite meant to see. The order lands in a status that sounds like somebody invented it at 2 AM because the damn enum needed to compile. The mess came from a workflow with no clear owner, and the service split gave the confusion DNS.

A useful boundary owns things that matter: its data, rules, decisions, and the states it can fail into. The internals can change without four neighboring systems changing in sympathy. If every order change still needs coordinated edits across payment, inventory, shipping, and email, I probably have one workflow wearing five name badges. Some workflows genuinely belong together, so that answer is fine. Pretending they are independent is the expensive part.

Before I split anything, I want a few annoying questions answered:

  • What data does this part actually own?
  • What decisions can it make with a small amount of outside information?
  • Can it fail in a state the product understands?
  • Can it deploy without a calendar invite?
  • Which business rule crosses the boundary?

The last question usually exposes the real cost. Suppose an order becomes confirmed only after payment is captured and inventory is reserved. Once payment and inventory live in separate services, I need a plan for inventory failing after the charge succeeds. Retries must avoid charging twice, idempotency keys have to survive the retry, messages need durable delivery, and reconciliation must handle the states I failed to imagine. Customer support also needs a way to see those states because customers have a gift for finding them. The separation may be worth all of this. I just want to see the bill before the boxes turn into running software.

Let the monolith prove it first#

This is why I like modular monoliths. I can keep one process and one deploy while making the boundaries visible in the code:

app/
  orders/
  billing/
  inventory/

Then I can enforce a few boring rules:

  • Orders avoid direct billing table reads.
  • Billing exposes commands instead of random structs.
  • Shared types stay small.
  • Imports stay acyclic.

If I cannot stop the orders module from rummaging through billing tables while both live in one repository, a network is unlikely to improve our manners. The bad import becomes an authenticated API call with retries, idempotency keys, schema versions, error mapping, trace IDs, and an on-call owner. An arrow looks inert on a diagram; in production it has behavior, latency, and several ways to fail on a Sunday.

I would rather make the boundary boring in code first. Outside modules lose direct table access, commands and queries become explicit, contract tests cover the interesting edges, and logs follow the whole workflow. Moving that module behind a network still takes work later, but at least we know the shape we are moving.

Clear ownership, different scaling needs, failure isolation, and real security or audit requirements can all justify the split. Billing may deserve its own service long before profile settings for exactly those reasons. A large folder can usually survive a refactor. Unclear ownership needs the slower work where people sit down and agree on which rules and data belong together, which is less exciting than creating a service and much cheaper than discovering the answer through timeouts.

TRIP COMPUTER / SESSION

TRIP A

Current drive.

A private counter for this browser session. Nothing here is transmitted or retained after the session ends.

Elapsed
00:00
Sections
0
Notes
0
Screens
0

Route/

Build platebe587d2
Chassis
v7.1.3
Revision
be587d2
Last serviced
09 Sept 2026

OWNER’S MANUAL / WTHRAJAT

OPERATING NOTES

How this thing moves.

The header behaves like a small mechanical system. Its readings respond to how you move through the site.

Throttle
Scrolling is input. Faster downward movement builds more momentum and engine speed.
Transmission
Upshifts follow sustained input. Scrolling upward slows and downshifts, and may briefly show reverse.
Idle
When input stops, RPM settles near 850 with mechanical drift. The gearbox eventually returns to neutral.
Tachometer
The needle always follows the reported RPM. It is never calculated from your position on the page.
Trip A
Session time, explored sections, opened notes and approximate screens travelled stay in this tab session.

Controls

Ctrl K
Search notes
?
Open this manual
Esc
Close an instrument
Tab
Move through controls