Architecture decisions

Modular Monolith vs Microservices for a Small Team

Compare modular monoliths and microservices for a small team: module boundaries, transaction design, operational costs, and evidence that justifies a split.

For a small team, a modular monolith is usually the better starting point: explicit business boundaries inside one deployable application, with fewer network failures and release dependencies to manage. Microservices become worthwhile when a specific boundary needs independent deployment, resource allocation or isolation, and the team can operate it. Choose from demonstrated constraints, rather than an assumed future traffic level.

Quick answer

  • Start with a modular monolith when one team owns most changes and business boundaries are still evolving.
  • Give each module a public interface and ownership of its data, even when modules share a database.
  • Use microservices when independent deployment or isolation solves a concrete problem.
  • Count deployment, monitoring, recovery and compatibility work alongside infrastructure costs.
  • Extract one well-understood capability before committing to a wider service split.

Separate code boundaries from deployment boundaries

A modular monolith organizes an application around business capabilities while deploying those capabilities together. Orders, inventory and billing can have separate interfaces, implementations and data ownership without communicating over HTTP.

Microservices add deployment boundaries. A service can be released independently, but its callers must tolerate its availability and contract changes.

Neither architecture follows automatically from repository layout. A monorepo can contain independently deployed services. Several repositories can still produce a system whose components must always ship together.

Microsoft's microservices architecture guide describes autonomous services organized around business capabilities, communicating through defined APIs. The useful property is autonomy. Splitting an application into network endpoints without that autonomy adds distribution while retaining coordination.

Decision areaModular monolithMicroservices
ReleaseModules ship togetherServices can ship separately
Internal callsUsually ordinary function callsNetwork calls or messages
Data consistencyShared transactions are available when designed for themCross-service workflows need explicit coordination
Resource allocationReplicate the application or separate suitable workersAllocate resources per service
Failure isolationModules usually share a process failure domainProcess isolation helps, but dependencies can propagate failure
RefactoringInterfaces and callers can change in one releaseContracts must tolerate independently deployed versions
OperationsFewer independently managed application componentsMore deployments, credentials and recovery procedures

A monolith can run multiple replicas behind a load balancer. Microservices do not automatically make an application faster, and a monolith does not require a single server.

Count the work around each service

The operational cost of a service includes everything required to change it safely and restore it when it fails.

For each proposed service, answer these questions:

  1. How is it built, deployed and rolled back?
  2. Which credentials and permissions does it need?
  3. How does a request get traced across its dependencies?
  4. What happens when it is slow, unavailable or returning stale data?
  5. Who investigates failures and performs recovery?
  6. How are its stored data and message backlogs restored?

Managed infrastructure can reduce provisioning work. It still leaves application decisions about retries, compatibility and recovery with the team.

Consider an illustrative checkout flow that calls inventory, pricing and payment services synchronously. A timeout does not necessarily mean the payment failed. The payment provider might have accepted the request before the response was lost. Retrying safely requires an operation identifier and a way to determine the existing outcome.

A modular monolith can face the same problem at an external payment boundary. The architectural choice determines how many additional internal boundaries have similar failure semantics.

Small teams should assess how much service operation fits alongside feature delivery and support. There is no universal developer count at which microservices become appropriate.

Make module boundaries enforceable

A directory named billing does not establish ownership if every other module can update its tables.

For an illustrative order-processing application, define responsibilities before choosing deployment units:

ModuleOwnsExposesMust avoid
OrdersOrder state and order linesPlace order, retrieve order statusUpdating stock tables directly
InventoryStock and reservationsReserve stock, release reservationChanging order status
BillingPayment attempts and payment stateRequest payment, retrieve payment outcomeDeciding inventory allocation

These are design examples, not a description of a deployed portfolio project.

Apply the boundary through the language and build system: separate packages, restricted exports, project references or dependency rules. Add an automated check that rejects imports from another module's internal implementation.

Keep database access behind the owning module. A shared relational database can use separate schemas or clearly owned tables. Where practical, database permissions can strengthen the separation.

Microsoft's data guidance distinguishes sharing a physical database server from sharing schemas or tables. For independent services, direct access to each other's tables creates coupling that undermines independent change.

Inside a monolith, cross-module joins remain an explicit trade-off. They may simplify reporting, but they also create dependencies on another module's representation. Put such queries behind a documented reporting interface and record that dependency before treating the module as extractable.

The same principle applies within a growing ASP.NET Core Minimal API application: endpoint organization helps navigation, while module ownership determines which code may change which business state.

Draw the transaction before extracting the service

Transaction boundaries often reveal more than a component diagram.

Suppose accepting an order requires both creating the order and reserving stock. In a modular monolith with a compatible shared database transaction, the application can coordinate both module operations and commit them atomically. The modules can retain separate interfaces without giving up that transaction.

After separating inventory into a service with its own database, an ordinary local transaction cannot cover both writes. The design needs explicit intermediate states and recovery behavior.

An illustrative workflow might be:

  1. Orders records an order as awaiting stock.
  2. Inventory accepts a reservation request with a stable operation identifier.
  3. Orders records acceptance after receiving the reservation outcome.
  4. A rejection moves the order into a failed state.
  5. An unresolved timeout leaves the order pending until reconciliation establishes the outcome.

The pending state must make sense to support staff and users. Define how long it may remain unresolved, who can repair it, and whether a reservation expires.

Persist the intention to publish

Another failure occurs when an application commits a database change and then publishes an event. If the process stops between those actions, the state changes without notifying consumers.

The transactional outbox pattern documented by AWS addresses this dual-write problem: persist the business change and an outbox record in the same transaction, then publish the record separately.

Publication can happen more than once. Consumers therefore need duplicate handling, and ordering requirements need their own design. An outbox makes the publication intention durable; it does not make a cross-service business operation atomic.

This matters for monoliths too, whenever they publish durable messages or call external integrations. The related webhooks versus polling discussion examines delivery and reconciliation at integration boundaries.

Require evidence for a service split

A useful extraction proposal names the current constraint, the expected benefit and the new responsibilities.

Proposed reasonEvidence to collectCheaper option to evaluate first
One workload needs different resourcesCPU, memory and queue behavior by workloadA separately scaled worker using the same application code
Releases interfere with each otherExamples of blocked releases and their causesBetter module boundaries and release checks
A capability needs isolationA defined fault or access boundaryProcess separation and restricted credentials
Independent ownership is necessaryA clear owner with deployment and support capacityExplicit module ownership
The domain is becoming complexDependency cycles and repeated cross-module changesRevising the domain boundaries

A background worker is a useful middle option. It can process queued work separately without requiring every business capability to become an independent service. If it shares application internals and releases, document that coupling honestly.

Microservices can be appropriate from the start when boundaries and operating requirements are already clear. A workload requiring a distinct access boundary or runtime may justify separation even for a small team.

Martin Fowler's “Monolith First” argument emphasizes the difficulty of identifying stable boundaries early. It also cautions against assuming that any monolith can later be split easily. Preserving boundaries requires ongoing work.

Record the decision and its reversal conditions

An architecture decision record should explain what would make the current choice wrong.

For a modular monolith, record the module ownership rules, known shared transactions, reporting dependencies and acceptable failure coupling. Name the evidence that would trigger reconsideration, such as a workload repeatedly exhausting application resources.

For an extraction, record contract compatibility, timeout behavior, duplicate handling, monitoring, rollback constraints and ownership of reconciliation.

At Legacies, my Technical Lead responsibilities cover backend architecture, integrations and product stability. Those responsibilities connect directly here: a deployment decision must include how the system changes and recovers. Selected work provides context for the engineering scope; discussing a project is useful when there is a concrete boundary, integration or operational constraint to assess.

Frequently Asked Questions

Is a modular monolith just a monolith with folders?

Folders alone are insufficient. A modular monolith needs explicit interfaces, data ownership and checks that prevent modules from bypassing those interfaces.

Can a small team operate microservices successfully?

Yes, when the service boundaries solve real problems and the team can support them. Existing deployment and observability infrastructure helps, but application-level compatibility and recovery still need owners.

Does a modular monolith prevent independent scaling?

It limits independent scaling of modules that run together in the same process. The application can still run multiple replicas, and suitable background workloads can move into separately scaled workers.

Should every module eventually become a microservice?

No. A module can remain valuable as an internal boundary indefinitely, and extraction should have a benefit that justifies the additional operating work.

Can microservices share a database?

They can share physical database infrastructure while retaining private data ownership. Reading or writing each other's tables ties services to shared representations and makes independent changes harder.

What should be extracted first?

Choose a capability with a clear contract, limited transactional coupling and a demonstrated need for separation. Define its failure behavior and recovery procedure before moving it across a network.

About the author

Alexandru Talnaci

Full-stack and systems engineer in Bucharest. Co-founder and technical lead at Legacies, working across web applications, integrations and C++ systems.

Explore the projects behind the notes
Back to all notesSubscribe via RSS