All posts

Monolith or microservices? Choose boundaries through changes, data, and operations

· 8 min read

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.

  1. Trace recent changes and record waits for reviews, integrated environments, and releases.
  2. Inspect code and tables that changed together. Ask whether the same business rule drives those changes, rather than grouping by folders alone.
  3. 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 capabilityAuthoritative data and updatesBoundary agreement
OrdersOrders module updates order stateConfirmation, cancellation, and duplicate requests
InventoryInventory module updates available stock and reservationsReservation result, expiry detection, cancellation, and retries
NotificationsNotifications module updates delivery stateDuplicate 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.

ConcernModular monolithSeparate services
Changes and releasesInternal separation with a shared deployment unitSeparate deployment when compatibility permits
ConsistencyMay use transactions within the same databaseDesign failures, retries, and compensation across databases
CapacityTypically scale the application together; consider isolating heavy workCan scale components with different loads separately
Failure impactShared process and resource effectsOpportunities for isolation; synchronous dependencies can propagate failure
OperationsFewer deployment, monitoring, and investigation unitsEach 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.

Example dependency rules to implement with language-appropriate checks
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 tests

Static 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

One asynchronous notification design, including failures between persistence and delivery

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.

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.

Primary sources for boundary design and verification

Tips this post digs into