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:
OrderServicePaymentServiceInventoryServiceShippingServiceEmailService
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.