The decision between modular monolith vs microservices in PHP is not determined by the number of modules, the age of the code, or the popularity of an architecture. An enterprise application can grow healthily within a single deployment if it maintains clear boundaries. Conversely, splitting it prematurely can turn simple internal calls into a network of contracts, queues, retries, and coordination issues.
The useful question is not “should we use microservices?”, but “which business capability needs to evolve, fail, be deployed, or scale independently, and can we bear the cost of operating it that way?”. The answer should start from the domain and actual operations, not from a target diagram.
Functional growth does not require separate services

Adding integrations, asynchronous processes, or product areas does not mean that each one must have its own service. A monolith can contain well-bounded modules, background jobs, queues, and adapters for external systems without losing operational coherence.
The first step is usually to reduce internal coupling. If a billing module directly imports order classes, modifies their tables, or knows internal inventory rules, the problem is not automatically fixed by moving code to another repository. It merely becomes network and data coupling.
A modular monolith aims for each capability to have an explicit internal interface, directed dependencies, and its own rules. In PHP, this can take the form of domain-based namespaces, application contracts, thin controllers, defined use cases, and adapters for persistence or external APIs. Deploying everything together remains compatible with these boundaries.
What to analyze before changing the architecture
Before discussing technology, identify business capabilities: for example, order management, catalog, identity, billing, document processing, or notifications. A capability does not necessarily map to an entity or a screen; it groups rules and decisions that should change for similar reasons.
- Owner: determine who maintains the rules, prioritizes changes, and responds to incidents.
- Data: identify what information each capability creates and governs, who can modify it, and which cross-capability reads it needs.
- Critical flows: map the path of a relevant operation, including validations, external dependencies, and asynchronous steps.
- Rate of change: distinguish frequent modifications from occasional changes. Frequency alone is not enough; what matters is whether it forces teams or releases to coordinate.
- Load profile: separate interactive traffic from tasks intensive in CPU, memory, storage, or third-party calls.
- Failure impact: establish what happens if a capability degrades for minutes or hours and whether the core business can continue operating.
This inventory reveals dependencies that often remain hidden: shared transactions, direct queries to other domains’ tables, duplicated rules in controllers, and scheduled tasks that update several domains. Extracting without resolving them produces services that are formally separate but functionally intertwined.
Six signals to keep a modular monolith
These signals favor strengthening the internal design rather than distributing responsibilities:
- Changes usually cut across several modules. If a business feature requires coordinated changes to orders, pricing, and billing, separation can multiply deployments and contracts.
- Immediate consistency is central. When an operation needs a single database transaction to preserve critical invariants, a distributed boundary adds complex compensation decisions.
- The team is small or shares ownership. Several services require operational discipline, on-call coverage, pipelines, versioning, and diagnostics for each unit.
- Load scales similarly. If components grow at the same pace and there is no isolatable bottleneck, separation provides no clear advantage.
- Domain boundaries are still unstable. Extracting a capability while its rules, vocabulary, and responsibilities are constantly changing establishes a premature boundary.
- Observability and automation are limited. Without structured logs, metrics, traces, alerts, and repeatable deployments, each network hop will make investigating an incident more costly.
Keeping the monolith does not mean accepting global code. The goal is for a module to evolve with logical autonomy even if it shares a process, repository, and release with others.
Six signals that justify an independent service
Extraction is more defensible when several of these conditions are combined, not when only one appears:
- There is a bounded and understandable responsibility. The service has a specific mission, cohesive rules, and its own domain language.
- It can own its data. It manages its storage and exposes agreed operations, events, or queries instead of allowing direct access to its tables.
- It genuinely needs independent deployments. Its change cycle must move forward without coordinating every release with the core.
- Its load is distinct. A conversion, search, file generation, or intensive calculation process may require different scaling and resources.
- Its failure can be isolated. The system can explicitly degrade if that capability does not respond, through retries, pending states, or deferred work.
- There is sufficient operational ownership. A team or owner can maintain its lifecycle, alerts, incidents, security, and compatibility.
An API alone does not turn a module into a microservice. Independence also depends on data, deployment, operations, and the ability to make decisions without relying on another application’s internals.
Costs that emerge when responsibilities are separated
A function call fails differently from an HTTP call, a queued message, or a remote query. After extraction, latency, timeouts, service-to-service authentication, rate limits, partial unavailability, and incompatible versions emerge.
The consistency model also changes. If one service confirms an operation and another does not receive or process the event, you must decide how to detect the state, retry without duplicating effects, and compensate when necessary. Event consumers must be idempotent; for example, processing a message twice must not issue two documents or charge twice.
Operations become more complex: request correlation, distributed traces, metric dashboards, log retention, secret management, backup policies, and recovery testing. In addition, every contract needs compatibility rules. Adding optional fields is usually less disruptive than changing semantics, removing fields, or reusing a status with a new meaning.
Transition architecture within PHP
The lowest-risk path is to modularize before extracting. Define an application layer for each capability, with use cases that receive commands or queries and return well-defined results. Hide database access behind repositories or ports when that represents a relevant dependency; do not turn every class into an abstraction without purpose.
The rest of the monolith should use the module through its internal public interface, not through its entities or tables. If asynchronous notifications are needed, publish domain or integration events from a controlled point. A transactional outbox pattern can help record the business change and the pending event in the same transaction, so that a later process delivers it reliably.
This phase makes it possible to verify whether the boundary is real. If the internal interface keeps growing, requires private objects from other modules, or needs shared transactions in every use case, it is not yet a strong candidate for separation.
How to define the first service boundary
The first service should have an easy-to-explain responsibility and limited dependence on the core. Document four elements before writing infrastructure:
- Responsibility: which decisions it makes and which ones are explicitly out of scope.
- API or events: inputs, outputs, errors, authentication, time limits, and idempotency.
- Data ownership: what it stores, which external identifiers it retains, and what information it queries through contracts.
- Compatibility: how producers and consumers will coexist during version changes, including delayed messages.
Avoid designing an API as a mirror of the tables. A contract should express business operations or facts, not expose persistence details that will block later changes.
Example: document processing without fragmenting the back office
Consider a PHP back office that manages case files and must generate, validate, and store documents. At first, processing can live as an internal module: it receives a generation request, records the job, runs an asynchronous task, and updates a status visible to the user.
Extraction becomes reasonable if generation consumes very different resources, needs specific conversion dependencies, receives its own spikes, and can work with a document request containing the minimum required data. The document service should not freely query case file tables. The back office can send an order with an identifier, applicable template, data version, and destination; the result returns as an event or queryable status.
Before that, it is important to clarify that a template defines the output structure, whereas a model may refer to domain data or an AI system. If AI were incorporated to classify documents, a bounded use case, evaluation with representative data, human review for sensitive decisions, data protection, cost control, and a manual or rules-based alternative when the provider fails would be needed.
Checks before extracting

Do not treat extraction as an isolated technical deployment. Define a gradual rollout for a controlled subset of operations, distinct from announcing the change to all users. Maintain a coexistence and rollback plan while behavior is being validated.
- Contract tests between producer and consumer, in addition to unit and integration tests.
- Metrics for latency, errors, retries, pending queues, duplicates, and time to complete the process.
- Correlation identifiers to track an operation across the monolith, queues, and service.
- Recovery procedures: safe re-execution, state reconciliation, backup, and restore.
- Explicit rules for functional degradation when the service is unavailable.
- An exit criterion: what evidence will demonstrate that extraction reduced a specific problem rather than merely shifting complexity.
The best architectural decision is the one that protects product evolution without imposing a disproportionate platform. A modular, measurable, and well-bounded PHP monolith is usually the right step until a capability demonstrates a verifiable need for independence.



