Monolith or microservices? Choose boundaries through changes, data, and operations
More services do not automatically mean less coordination. Compare a modular monolith with service extraction through orders, inventory, and notifications, including transactions, contract tests, and operational ownership.
Map where changes wait before drawing services
Suppose every delivery-date change needs orders, inventory, and notifications to coordinate a release. Three applications will not remove that dependency if the same meetings and synchronized releases remain. DORA treats the ability to change, test, and deploy without detailed coordination with other teams as a key property of loose coupling.
- Trace recent changes and record waits for reviews, integrated environments, and releases.
- Inspect code and tables that changed together. Ask whether the same business rule drives those changes, rather than grouping by folders alone.
- Separate architectural coupling from approval delays and staffing shortages. State which problem extraction addresses.
Choose the orders–inventory boundary around invariants
Suppose one database transaction currently ensures that orders are confirmed only for reserved stock. Moving orders and inventory into separate databases removes that existing transaction boundary. You need a new consistency design, such as a reservation state machine covering expiry, cancellation, and failures.
| Business capability | Authoritative data and updates | Boundary agreement |
|---|---|---|
| Orders | Orders module updates order state | Confirmation, cancellation, and duplicate requests |
| Inventory | Inventory module updates available stock and reservations | Reservation result, expiry detection, cancellation, and retries |
| Notifications | Notifications module updates delivery state | Duplicate events, retries, and failed-delivery review |
A shared database can still have explicit update ownership. When orders and inventory need one transaction, their public interfaces can participate in that transaction. Distinguish a logical module boundary from a separate process and database boundary.
Compare a modular monolith and services under the same conditions
A modular monolith deploys modules with defined interfaces and dependency rules as one application. Martin Fowler's Monolith First discusses the operational premium of services and uncertainty in early boundaries. Treat it as reasoning to assess alongside team experience and known boundaries, rather than a universal rule.
| Concern | Modular monolith | Separate services |
|---|---|---|
| Changes and releases | Internal separation with a shared deployment unit | Separate deployment when compatibility permits |
| Consistency | May use transactions within the same database | Design failures, retries, and compensation across databases |
| Capacity | Typically scale the application together; consider isolating heavy work | Can scale components with different loads separately |
| Failure impact | Shared process and resource effects | Opportunities for isolation; synchronous dependencies can propagate failure |
| Operations | Fewer deployment, monitoring, and investigation units | Each service needs monitoring, delivery, authentication, and recovery |
For this team, orders, inventory, and notifications could first become modules. If notification load or provider failures affect order acceptance, test the value of asynchronous separation. Orders and inventory can remain together until evidence justifies the consistency and operating costs of extraction.
Enforce boundaries through dependency checks and data ownership
Folders alone do not prevent orders from calling notification internals or updating its tables. Define public interfaces and prohibited dependencies. ArchUnit provides checks for Java package dependencies and cycles, among other code properties.
orders -> inventory.public
orders -> notifications.public
Forbidden: orders -> inventory.internal
Forbidden: orders -> notifications.internal
Forbidden: notifications -> orders.internal
Updates: Each module writes its own tables
Validation: Dependency checks + business-rule tests + critical integration testsStatic dependency checks do not guarantee that raw SQL respects ownership or that dynamic calls obey the rules. Include database access design, permissions where useful, reviews, and integration tests. If existing violations cannot be removed immediately, inventory them and stop new violations from accumulating.
Before extracting notifications, define communication and recovery
Save order and delivery intent in one database transaction → Dispatcher reads delivery intents and publishes events → Notification service handles duplicates using event IDs → Record provider outcomes; query or investigate unknown results
Saving an order and then calling notifications over HTTP leaves a crash window. Consider an outbox that saves delivery intent in the order transaction. Dispatcher retries can duplicate events, so consumers must handle duplicates too. A crash immediately after an external email send also shows why a local processed flag alone cannot guarantee no duplicate external effect.
- Contract: define event versions, required fields, ordering, and repeated events for one order. Combine contract tests with E2E coverage for critical journeys.
- Failures: define timeouts, retry limits, storage for unprocessable events, and a reprocessing owner.
- Operations: connect acceptance to delivery with IDs, monitor backlog age, enforce authorization, and establish response hours and backup ownership.
Measure extraction through delivery and operations
Compare delivery waiting time, synchronized-release frequency, incident scope, and recovery effort before and after extraction. If every change still requires joint testing and release, revisit contract and data coupling. When retaining a modular monolith, periodically reassess the cost of shared deployment and resources too.
In a review, explain the intended outcome: isolate notification backlog from order acceptance, establish update ownership and asynchronous contracts, then measure delivery waits and failure impact. That connects the chosen structure to its purpose and costs.