The case for microservices is usually made on scaling and team autonomy. Both are real. What gets left out is the fixed cost you pay from day one, which does not scale down for small teams.
An in-process call cannot fail halfway, cannot be slow because of a DNS problem, and cannot return a partial response. A network call can do all three. Every boundary you introduce needs timeouts, retries with jitter, idempotency keys so a retry does not double-charge someone, and a circuit breaker so one slow dependency does not exhaust every thread in the caller. That is real code, per boundary, and it is the code teams write last.
The moment order creation and payment live in different services with different databases, you cannot wrap them in a transaction. The replacements are worse: a saga with explicit compensating actions, or the outbox pattern where you write the event to the same database transaction as the state change and a relay publishes it. Both work. Both mean handling a state where the order exists and the payment does not, forever. If your domain has invariants that must hold across two entities, keeping them in one service and one database is not a failure of ambition.
Running a monolith locally is one process. Running twelve services locally is twelve processes, their databases, a message broker, and enough memory that a laptop starts swapping. Teams end up with one of three answers: docker-compose files that drift from production, shared development environments where one person's broken deploy blocks everyone, or per-developer namespaces in a cluster which works well and costs real money. Budget for this decision explicitly rather than discovering it in month three.
In a monolith, changing a function signature is a compile error you fix in the same commit. Across services it is a coordinated deploy. The workable rule is additive-only changes: add fields, never remove or rename them, never change a field's meaning, and keep old fields populated until you can prove nobody reads them. Consumer-driven contract testing catches breakage before deploy; without it you find out in production, in the one code path that only runs on the first of the month.
A stack trace no longer tells you what happened. You need distributed tracing with a propagated trace ID (OpenTelemetry is the default answer), structured logs that include that ID on every line, and log aggregation that can query across services. Set this up before the split, not after — retrofitting correlation IDs into twelve services that already exist is far more work than adding them to one that does not yet.
A modular monolith: one deployable, one database, but hard internal boundaries with module-owned schemas and no cross-module table access. You get the code organisation benefit immediately, keep transactions and local development simple, and the modules become the natural seams if you ever do need to extract one. Extracting a well-bounded module later is a week of work. Reassembling twelve services into a coherent system is a year.
Free tools, guides, and resources across the SPUNK13 network.
Visit spunk.bet400+ Free Tools