Apr 25, 2025·8 min

Modular Monolith or Microservices: Criteria and Migration

Modular monolith or microservices: how to choose an approach and migrate step by step without rewriting the system, with clear checks and risk control.

Modular Monolith or Microservices: Criteria and Migration

Where the choice really starts: the problems you need to solve

The discussion about a modular monolith versus microservices almost never starts with architecture. It starts with pain that prevents you from releasing changes, maintaining quality, and passing checks. The first step is simple: honestly name 2–3 main problems and measure them. How many releases per month do you do now, how many incidents do you catch, how much time is spent on approvals, tests and rollbacks.

Most often microservices are considered for the same set of situations. Releases become infrequent and risky: any change touches half the system. Teams interfere with each other in one repository and one database; queues form to "merge" code. The business needs different speeds for different modules: some features change weekly, others are almost static. Requirements for data isolation, audits, access control and regulation appear. If the product runs 24/7, an incident in one place can start taking down the whole service.

In corporate development there is additional context: many people and roles (business, security, IT, procurement), long approval cycles, required documentation, and support and accountability requirements. So often predictability and change control matter more than the "trendiest" architecture.

Rewriting from scratch almost always misses deadlines for a simple reason: while you build the new world, the old one keeps running. Requirements change, bugs accumulate, and teams end up maintaining two systems without a clear cutover.

A useful initial goal is to choose an approach that allows small steps: improving module boundaries, reducing coupling and extracting problematic parts only when it gives a clear effect for the business and operations.

Briefly about the approaches: modular monolith and microservices in plain language

A modular monolith is a single application and single deployment, but internally it is divided into clear parts (modules) with strict boundaries. Each module has its area of responsibility: its business rules, its data models and its API for others. The main rule: modules communicate only through agreed interfaces, not by "I'll just read your table or class directly."

Microservices are a set of separate applications. Each service can be deployed and updated independently, has its own data and lifecycle. This gives flexibility for large teams and different change cadences, but the cost is more daily operational work: monitoring, networking, API versions, security, incidents and on-call schedules.

How to tell what you have now? If "everything is connected to everything," a common entity layer is used everywhere, and any edit requires running regression over half the system — that's a monolith without modules. If you can already name 5–10 domains (for example, "customers," "orders," "invoices"), and changes usually affect one domain rather than everything, you are close to modularity even if deployment is still unified.

Some useful decisions can be made without big code changes. Usually it’s enough to agree on ownership rules (each domain has a responsible team/owner), tighten the release process (release smaller changes more often), start recording dependencies (which parts are actually coupled) and identify critical business flows to test first.

This framework helps remove ideology: "modular monolith or microservices" isn’t about fashion, it’s about how much independence you need and how much complexity you are willing to operate every day.

Selection criteria that matter in a corporation

In a corporation, architecture is chosen based on people, processes and risks. The same product can be an "ideal monolith" in one company and an "unbearable monolith" in another.

First criterion — teams. If you have 2–3 teams that constantly touch the same code and release together, a modular monolith is often simpler and faster. If there are many teams that can work independently (different domains, different cadences, separate release windows), microservices provide organizational freedom but only with discipline and mature operations.

Second — frequency of changes. Look at which modules change weekly and which live for years. Often the best approach is to "extract only the hottest": the rest does not have to become services.

Third — reliability and cost of downtime. If failure in one area must not "bring everything down," service isolation helps. But sometimes good boundaries inside a monolith plus infrastructure-level redundancy are enough.

Fourth — integrations. The more external systems (government registries, banks, ERP/CRM) and the less stable they are, the more important it is to isolate risk points and design queues, retries and rate limits. This can be done in a monolith, but service boundaries often make these risks clearer.

Fifth — security and audit. Corporate requirements for access control, logging and audits are easier to implement when responsibilities are clear: who has access to data, where logs are written, who approves changes.

Finally — timelines and budget. Microservices almost always increase operational complexity: monitoring, tracing, deployment, incidents and 24/7 support. If you don’t have resources for that, the choice often falls to a modular monolith at least for the next year.

A simple decision matrix: how to decide without arguments

Arguments usually start with loud slogans and end with missed deadlines. More practical: pick 4–5 criteria, honestly assess both approaches and record your business priorities. Then the question "modular monolith or microservices" becomes a clear decision rather than an opinion battle.

CriterionModular monolithMicroservicesYour priority (1-5)
Speed of product changesFast if modules are clearly separatedFast in separate domains, but coordination costs more
Reliability and fault toleranceEasier to manage as a whole, but single-failure riskFailure isolation, but more operational complexity
Teams and ownershipConvenient up to 1–3 teamsRequires autonomous domain teams
Infrastructure and observabilityMinimal monitoring requiredRequires logs, metrics, tracing and alerts
Integrations and dataEasier transactions and consistencyData is harder, needs event/saga planning

There are threshold questions after which microservices are probably premature. For example: releases occur less often than every 2–4 weeks and the business accepts that; there’s no dedicated operations role/team (SRE/DevOps) and everything relies on a single hero; no centralized monitoring or clear incident response process; domain boundaries are disputed and not defined in business terms; testing is mostly manual and already noticeably slows delivery.

If you want 80% of the benefit faster, a modular monolith often wins: introduce module boundaries, forbid direct dependencies, and tidy up contracts. That alone reduces chaos without a sharp rise in operational costs.

Signs you’re ready for microservices are also checkable: domain owners and teams that can release independently; metrics, logs and tracing configured; on-call rotations and alert rules in place; automated releases with safe rollback and canary capabilities; a clear data strategy (where strict consistency is required and where delays are acceptable); time for operations and support budgeted rather than treated as overhead.

If you stay with a modular monolith: how to make it real

A modular monolith works only if modules are truly separated. Otherwise it’s just one big codebase where "everything depends on everything" and changes slow down every month. If you’re choosing between a modular monolith and microservices, first make the monolith manageable and predictable.

First step — inventory domains. List major functions (for example: sales, warehouse, billing, reporting, integrations) and assign an owner for each area. The owner is responsible not for people but for decisions about boundaries: what’s inside, what’s outside, and which data is the source of truth.

Boundaries and dependencies

Next, define what counts as a module and what should remain shared. "Shared" usually means infrastructure (logging, config, queue access) and rarely common reference data. Everything else is better kept inside modules.

Dependency rules must be not only written but enforceable. A few basic bans are often enough: a module calls the outside only via its public interface (module API); direct calls to another module’s internal classes are forbidden; data from another module is read through its access layer, not via direct queries; any new inter-module dependencies are discussed and recorded. These rules are best enforced in builds (architectural tests or static checks).

Technical stop-lists and quick wins

The most common holes that erode modularity should be immediately forbidden and added to a stop-list. Typically these include a shared database as the single source of truth for all modules, "shared utilities" that become a dumping ground, shared data models that drag half the system with them, and a common service layer where business logic accumulates.

For quick visible results, isolate the most problematic area: where incidents are concentrated or changes break neighboring features. Extract it into a separate module with a clear API, forbid direct table access and add a few contract tests. This delivers noticeable benefit without rewriting the whole app.

Migration plan without rewriting: a step-by-step scheme

Operational reliability audit
We will check logs, metrics, alerts and readiness for on-call duty before scaling services.
Order an audit

The main migration principle: first agree on boundaries and contracts between parts of the system, and only then physically separate code and infrastructure. Otherwise you simply move chaos from a monolith into a set of services.

Start with a domain map: which modules are independent by meaning (customers, orders, inventory, reporting) and where the "seams" are. Those seams become contracts: APIs, events, file exchanges, queues. It’s important to forbid internal calls directly to another module’s database or classes.

Step-by-step scheme

  1. Choose a pilot module with clear value and moderate risk. A good candidate changes frequently but doesn’t stop the business if it fails (for example, notifications, reference data, reporting storefronts).

  2. Stabilize interfaces. Freeze API versions or event formats, add contract tests and enforce a rule: new integrations only through the contract.

  3. Apply the Strangler pattern: add a bypass layer around the old code. New requests go to the new module or service, old ones stay in the monolith. Traffic shifts gradually.

  4. Extract one service completely and bring its operations to a normal level: separate release cycle, monitoring, alerts, tracing, and a clear rollback (feature flags, route switching, fallback to the old handler).

  5. Repeat in iterations and measure delivery speed and quality: time from task to release, number of incidents, share of manual operations.

Small example

In a corporate procurement approval system, start with a notifications module. It gives quick wins (less manual communication) and is easy to separate with an event contract like "event: request moved to new status." After that, extracting more critical parts becomes easier because seams have been tested in real operation.

This approach helps make the decision "modular monolith or microservices" evidence-based rather than ideological, using pilot results and team readiness.

Data and transactions: how not to break business processes

Data is the most common cause of failures when moving from a monolith to services. Code can be distributed, but a shared database often remains. Then responsibilities blur: who changes a table, who’s responsible for data quality, and who ensures reports and scheduled processes don’t break.

During transition it’s sometimes acceptable to keep one shared source of data, if access rules are defined in advance. For example, in a request tracking system the new service handles only its own operations and may not directly modify other teams’ tables.

A minimal set of rules that usually saves you looks like this: each table (or set of tables) has an owner (team and service); only the owner updates records and others read via APIs or views; schema changes go through a common approval process; critical fields and reference data are covered by quality checks; a unified identifier dictionary is recorded (what counts as a key).

Next step — the service gets its own database. Then changes don’t "leak" outwards, but synchronization is needed. The clearest option is events: the owning service writes locally and publishes facts (for example, "order created"), and other services update their local copies.

Distributed transactions usually cause more pain than value. Instead, use sagas: a business operation is split into steps, and if one step fails, compensating actions are executed. Simple example: "create invoice -> reserve item -> send notification." If reservation fails, the invoice is cancelled.

Change data schemas in safe stages: add a new field, populate it in the background, switch reads and writes to the new field, then remove the old one later. This avoids a big single "day X" when everything breaks and allows rollback without stopping the business.

Operations and reliability: what to prepare in advance

Corporate procurement supply
We will agree requirements and completeness for corporate procurement according to standards.
Submit a request

Microservices often speed up changes but almost always increase operational complexity. A network appears between components, so failures arise not only in code but also from timeouts, load balancing, queues, DNS, API version mismatches and contract disagreements.

It’s important to accept early: reliability becomes an attribute of the system, not of a single service. If one service stalls, it can pull in a chain of calls and users see a global outage.

Minimum observability you can’t do without

Set a basic observability stack before you have many services. Then you find problems in minutes rather than days.

  • Centralized logs with a common format and correlation by request id.
  • Metrics (latencies, errors, load) and clear dashboards.
  • Distributed tracing for call chains across services.
  • Alerts with thresholds and escalation rules, not just noise.
  • Health and readiness checks so deployments aren’t surprising.

Reliability practices that pay off immediately

Network failures will happen, so build protection into protocols and business operations. If a payments service sends an event to a queue, retrying must not charge money twice.

Practically always useful: retries with limits and delays plus default timeouts; idempotency for key operations (re-sending a request doesn’t change the result); queues for long or bursty tasks with retry/deadline control; centralized configuration and secret management without manual edits on servers.

Decide separately "who’s on call at night." You need on-call shifts, access to logs and metrics, an incident protocol and a simple postmortem process. In corporations this often matters more than framework choice: without clear responsibility, microservices quickly become chaos.

Typical mistakes and traps during migration

The most common mistake is to start migration with the biggest, most tangled module. It seems "critical," but it usually hides dependencies and business exceptions. Result: the project stalls, trust drops, and the team returns to old ways. Better to start with a small, clear contour with obvious boundaries.

Another trap: "we made microservices" but kept one shared database with no rules. Services are micro only in name; in reality they break each other’s schemas, fight over transactions and make releases dangerous. If a shared DB is unavoidable temporarily, enforce strict agreements: who modifies which tables, how migrations are coordinated, and what data is the source of truth.

A frequent failure is building complex infrastructure too early when there’s no team to maintain it. Orchestration, observability, CI/CD, secret and access management help only if people and processes use them daily. Otherwise they become an expensive black box.

Signals you’re in a trap: releases fail due to incompatible API versions and no compatibility rules; production sees sudden timeouts and retries because the network added delays; services argue about changes because domain ownership and decision-makers are undefined; functionality is extracted but critical operations are left distributed without sagas, compensations and clear statuses; migration is measured by number of services rather than reduced risk and faster changes.

In corporations it’s especially important to agree in advance on ownership and release rules. This usually distinguishes a careful phased migration from endless refactoring.

Short checklist before starting migration

Before arguing about whether a modular monolith or microservices are better, record the basics. This checklist helps avoid starting a "move" only to find that goals differ, nobody wants to touch the data, and there is no rollback plan.

Check that you have ready before the first backlog task:

  • 3–5 measurable goals: releases weekly instead of monthly, fewer incidents, isolated change risks, shorter recovery time.
  • A domain map and a list of modules with owners: who decides, who’s responsible for quality, who is on call for failures.
  • Rules for inter-module dependencies that are enforced in builds: forbidden direct imports, DB access only through a layer, tests for module boundaries.
  • A chosen pilot candidate and success criteria: small but valuable module with clear inputs/outputs; a clear definition of victory and stop signals.
  • A minimum observability and rollback setup: error/latency metrics, centralized logs, alerts and a clear rollback procedure that avoids manual panic.

Separately agree a data plan: where the sources of truth will be, how to migrate schemas without downtime, which operations must remain transactional. If this is not resolved up front, migration often stalls at the first database dispute.

A practical test: try to describe the first pilot on one page. Which requests will go to the new module, how to enable/disable the new route, and how the team will know it’s better.

Example scenario: how a company migrates in parts

Pilot without stopping the business
GSE.kz will help launch a pilot module on infrastructure close to production.
Start a pilot

Imagine a corporate system with employee requests, contract approvals, payments, executive reports and notifications. Everything runs in one application. Formally it’s a monolith, but it contains many "modules" that frequently interfere.

The problem is tempo rather than code. Releases happen quarterly because a change in requests unexpectedly breaks reports, and a fix in payments requires synchronizing three teams. Everyone fears touching the system and tasks pile up.

The team decides to stop arguing about modular monolith vs microservices and start with boundaries. In 3–4 weeks they turn the monolith into a true modular monolith: they describe domains (requests, contracts, payments, reports, notifications), lock dependency rules and assign owners. They also introduce a rule: inter-module calls only via public interfaces, no direct table access.

Next, they pick a pilot to extract. Often notifications or reporting are chosen: they have less transactional complexity but big benefit from independent releases. For example, the notifications service starts sending emails/messages itself, and the monolith calls it via an API. For reliability they keep a fallback: if the service is down, events are queued and sent later.

To measure effect, they pick metrics and record a baseline:

  • lead time from task to production
  • number of incidents after release
  • rollback or disablement time
  • number of inter-team blockers

After three months success looks simple: one service (notifications) is released independently weekly and the monolith sees fewer cross-cutting edits. Teams argue less because they can see numbers, not promises.

Next steps: how to start without stopping the business

Start with real pains, not technologies. Gather 5–10 concrete problems: releases are too rare, changes break neighboring features, teams block each other, it’s hard to scale a single module, incident queues grow. Map these pains to your decision matrix and verify that the chosen path addresses root causes, not symptoms.

Even if the goal sounds like "modular monolith or microservices," the first step is often the same: agree on boundaries and rules. Introduce clear domains, module owners, API contracts, forbid direct access to others’ tables, and standardize logging and error handling. This makes the monolith manageable and prepares for careful service extraction.

To avoid stopping the business, pick one pilot that can be separated with minimal risk. Good candidates are reporting, notifications, reference catalogs or an integration gateway. The pilot should serve as a model: use it to lock standards, CI/CD templates, testing, monitoring and rollback patterns.

Prepare basic operations before launching the pilot, otherwise the "first win" becomes more incidents. At minimum you need observability (metrics, logs, tracing and clear alerts), resources (compute, storage, quotas and scaling plans), backups and recovery scenarios, safe releases (fast rollback, feature flags) and clear ownership (who’s on call and who approves changes).

A practical start: in 2–4 weeks create a domain map and modularity rules, simultaneously set up monitoring and environments, then build one pilot component properly. If you lack architecture, integration and infrastructure expertise, corporations often bring in a systems integrator. For example, GSE.kz (gse.kz) combines integration with delivery and corporate infrastructure support, including servers and workstations, so the pilot runs in conditions close to production.

FAQ

Where to start choosing between a modular monolith and microservices when everyone is arguing?

Start with 2–3 measurable pains: how often you release, how many incidents you catch after releases, and how long it takes from task to production. If rare releases and high change risk hurt the business, first tidy up boundaries and processes, then decide on the desired level of service-orientation.

How do you tell whether we have a modular monolith or just a "plain monolith"?

A modular monolith is a single application and single deployment that contains real modules with strict boundaries and clear interfaces. If you have shared entities everywhere, direct access to neighbor tables, and any change forces regression across half the system, that’s not a modular monolith—it’s a monolith without boundaries.

How many teams is the threshold for considering microservices?

As a rule of thumb, for 1–3 tightly coupled teams a modular monolith is usually better because it’s cheaper to operate and simpler to release. Microservices become justified when there are many teams, domains are independent, and each team can truly release and be on-call autonomously.

How to choose the first module for a pilot to avoid failure?

Start with a ‘hot’ and problematic contour rather than the whole application: notifications, reporting, reference data, or a separate integration gateway. A good candidate has clear inputs and outputs, moderate risk if it fails, and noticeable benefit from independent releases.

How to migrate to microservices without rewriting the system from scratch?

Don’t try to do everything at once and don’t start with the largest, most tangled block. First fix the contract (API or events), forbid direct access to other modules’ tables and internal classes, add simple contract tests, then extract outward using the Strangler pattern and gradually increase traffic share.

Is it possible to temporarily keep a shared database while extracting services?

Yes — during a transition it’s acceptable to keep a shared database if strict ownership rules are set in advance. Assign table owners, forbid other modules/services from modifying those tables directly, and agree on a schema migration process. Without that, releases quickly become dangerous because teams will fight over the database.

What to do about transactions and data consistency when moving to services?

Transactions are convenient in a monolith, but distributed transactions usually cause more pain than they solve. It’s more practical to design business operations as sequences of steps with statuses and compensations (sagas) so that a failure in one step results in a clear, reversible outcome.

What operational capabilities are needed in advance so microservices don’t turn into chaos?

Without basic observability and clear rollback procedures, services will only add pain: incident root causes take longer to find, and timeouts and retries hide problems. Before the number of services grows, set up centralized logs with correlation, latency/error metrics, basic tracing, alerts with clear thresholds, and a safe rollback process.

What typical mistakes are made when moving to microservices?

Common pitfalls: "services on paper" — separate deployments but one shared DB without rules; incompatible API versions with no backward compatibility; manual releases with no reliable rollback; missing domain owners and no on-call responsibilities. If you see these signs, stop and first strengthen boundaries, contracts and processes.

Which metrics show whether the chosen approach actually works?

Measure a baseline and agree on metrics: lead time from task to release, release frequency, number of incidents after releases, recovery time, and share of manual operations. Success is when these numbers improve — not simply when the number of services increases. Use these metrics to decide whether to continue extraction or stabilize on a modular monolith.