IHOR.DEVKNOWLEDGE_BASE
MENU

~/blog / Architecture / modular-monolith-vs-microservices-for-web-apps.md

// guide · 4 min read

Modular Monolith vs Microservices for Web Apps

Compare a modular monolith with microservices using team ownership, transaction boundaries, independent deployment, failure isolation, and operational costs.

Quick answer

Prefer a modular monolith when one team owns a product and its capabilities can be released together. Use microservices when independently owned capabilities need independent deployment, scaling, or fault isolation and the team can operate distributed contracts. A monolith describes a deployment unit, not a requirement for tangled code. A microservice describes an independently operated boundary, not merely a small folder with an HTTP endpoint.

Practical implementation steps

1. Define modules from business ownership

Group rules that change together, such as booking acceptance or invoice calculation. Expose a deliberate interface and keep internal tables and implementation private to the module where practical. A caller should request a business operation rather than modify another module’s records directly. This creates a boundary inside one process before the team adds network failure, retries, and independent versioning.

2. Map invariants and transaction boundaries

A single database transaction can protect related updates in a monolith. Across independently owned databases, that same invariant requires a different workflow, compensation, or coordination mechanism. Identify which promises need immediate consistency and which can converge later. Do not split a tightly coupled write merely because separate services look cleaner on a diagram; the runtime coordination may become harder than the original module boundary.

3. Prove the reason for independent operation

Name the capability blocked by shared release, its owner, and the workload requiring isolation. Then include contracts, authentication between services, deployment compatibility, tracing, on-call ownership, and recovery in the cost. A service that still shares every table and must deploy in lockstep has not acquired useful independence. Start extraction with one boundary that can tolerate the distributed failure modes.

Implementation example

This TypeScript interface represents a booking capability callable inside a monolith. It does not prescribe a network transport or hide storage operations in the client. If later extracted, the same business outcomes can inform an HTTP or message contract, but serialization, timeout, authentication, and versioning still require explicit design. Domain interfaces make an extraction possible; they do not make it free.

type ReservationResult =
  | { kind: "accepted"; reservationId: string }
  | { kind: "unavailable" }
  | { kind: "denied" };

export interface Reservations {
  reserve(input: {
    verifiedCustomerId: string;
    slotId: string;
    requestId: string;
  }): Promise<ReservationResult>;
}

Tradeoffs and alternatives

A modular monolith shares runtime resources and release timing while reducing network coordination. Microservices can isolate workloads and release ownership but add partial failures, duplicated operational tooling, and compatibility windows. A separate worker for slow jobs can be a useful intermediate choice without turning every domain into a service. Select the topology for a measured ownership or workload constraint, not a hoped-for scale story.

Common mistakes

Avoid confusing code modules with deployed services, sharing one writable schema across supposedly independent owners, or creating long synchronous request chains. A retry can amplify an overloaded downstream service. An event does not automatically provide exactly-once effects or complete ordering. Conversely, do not keep a monolith unstructured because it has one deployment: uncontrolled imports and cross-module writes make later changes risky.

Production checklist

  • Modules have explicit business interfaces and owners.
  • Invariants identify immediate versus eventual consistency.
  • Each service split solves a concrete release or workload problem.
  • Distributed failures and compatibility windows are documented.
  • Operators can trace and recover one cross-boundary workflow.

FAQ

Is a monolith unsuitable for a growing product?

No. Modular ownership, appropriate resource scaling, and a sound data model can support growth. Extract when a specific constraint justifies the distributed costs.

Can services share a database?

They can, but shared writable ownership couples deployment and rules. Document the coupling and avoid claiming isolation that the data contract does not provide.

Next step

Create one clear module boundary in the current codebase and measure the problem an extraction would solve. Include operational work in the comparison. Keep the boundary in-process until independent operation supplies a concrete product or team benefit.

Primary sources

Ihor Kharchyshyn

Independent full-stack product engineer writing source-backed implementation guides for browser extensions and modern web products.

Need this implemented in a real product?

One connected implementation across the interface, application logic, APIs, and data layer.