ADR-0002 — Modular Monolith over Microservices
Status: Accepted · Date: 2026-07-20 · Deciders: Amir Haroun (Tech Lead)
Related: arc42 §5, CON-14, QAS-MOD-03, R-08, CONF-04
Context#
Qwizin is a multi-sided SaaS marketplace with twelve identifiable business capabilities, six actor types, and an investor-facing Phase-2 roadmap (CONF-04) several times the MVP's scope — POS/PMS, inventory, CRM, reservations, and government integration.
The architect explicitly asked whether microservices were the better choice. The constraints that bear on the answer:
CON-01— low-cost implementation, budget-sensitiveCON-15— Kubernetes as the orchestration target- One delivery vendor (Afaq Al SPL), single team
- Aggressive growth target: ~10⁴ accounts and hundreds of concurrent video sessions in 12–18 months
CONF-04— a large deferred scope that will arrive
Decision#
A Laravel modular monolith with enforced internal boundaries, plus a small number of separately-deployed sidecars.
A component is extracted only when it has a genuinely different scaling profile, runtime, or failure domain:
| Extracted | Why |
|---|---|
| Document Renderer | Headless browser runtime; hundreds of MB per render would distort API pod sizing. Also the only component needing the Arabic font/shaping stack. |
| Malware Scanner | ~1 GB signature database; deliberately isolated as the component that touches untrusted input first |
| Search Engine | Stateful, with its own memory and disk profile |
| Media / TURN (conditional) | hostNetwork requirement makes it architecturally unlike everything else in the cluster |
Queue workers and the scheduler run from the same image as the API with a different entrypoint — deliberately not separate services.
Rationale#
Why not microservices#
1. There is no independent scaling need across business capabilities. Booking, templates, learning, and directory browsing have near-identical, modest load profiles. The components that genuinely differ have been extracted — which delivers the actual engineering benefit microservices offer, without the distributed-systems cost.
2. No team-topology justification. Microservice boundaries pay off when they map to independently-deploying teams. There is one team. Service boundaries would create coordination overhead across a boundary that doesn't exist organisationally.
3. Distributed transactions would be a net loss. The booking ↔ payment ↔ video flow already requires careful state management — it spans a slot lock, a payment authorization, a counterparty acceptance, and a session lifecycle (CONF-02). Splitting it across services converts a database transaction into a saga, adding compensating-action complexity for no benefit.
4. It opposes CON-01 and QAS-OPS-01 directly. Microservices multiply the operational surface a small team must run — more deployables, more network failure modes, more observability surface. CON-15 already raises the operational floor; microservices on top would raise it past what the team can carry.
5. The option is retained anyway. QAS-MOD-03 mandates CI-enforced boundaries, so Phase-2 extraction stays cheap (≤ 20 person-days per module). The optionality is preserved without paying for it now.
Why not a plain monolith#
CONF-04 is the deciding factor. A conventional Laravel application — where every module hangs relations off User, cross-module foreign keys are routine, and Eloquent traverses freely across domains — makes Phase-2 a rewrite rather than an extension. The modular discipline is the insurance premium on a roadmap the business has already committed to investors.
The specific failure this guards against#
Modular monoliths do not fail because boundaries were drawn wrong. They fail because nothing stopped them eroding. Eloquent's ergonomics are precisely what dissolve boundaries: $user->bookings->first()->consultant->company spans four modules in one line, compiles fine, reads naturally, and is unextractable.
Therefore the decision is inseparable from its enforcement — see Consequences.
Consequences#
Required, not optional#
This decision is only valid if the following ship. Without them, the modular monolith degrades to a plain monolith within a year and the Phase-2 estimate becomes fiction.
| Requirement | Detail |
|---|---|
Public/ vs Internal/ namespace split per module |
arc42 §5.2 |
Deptrac in CI with --fail-on-uncovered |
⚠️ Package moved: qossmic/deptrac is abandoned → use deptrac/deptrac |
Architecture tests — only Identity references User; no models cross boundaries |
arc42 §5.6 |
| No cross-module foreign keys, enforced by a schema test | — |
Transactional outbox + own EventBus interface, week one |
~1 day; the cheapest extraction insurance available |
Accepted trade-offs#
| Cost | Mitigation |
|---|---|
| No cross-module referential integrity | Domain events for cascades; a reconciliation job reporting orphans as a metric, not a failed write |
| Local projections duplicate some data | Each module owns exactly what it needs; no distributed joins |
| More boilerplate — DTOs, contracts, events | Extraction stays a refactor rather than a rewrite |
| Slower initial development | Deliberate: R-08 is the only risk marked effectively irreversible |
| Whole-application deploys | Acceptable at this team size; also simpler than coordinated multi-service releases |
Revisit when#
- A capability develops a genuinely divergent scaling profile (Phase-2 LMS or procurement are the likely candidates)
- Team topology changes — multiple independent teams with separate release cadences
- A regulatory boundary demands isolation (e.g. if
R-01forces payment handling into a separately-audited deployment)
If any of these arrive, the extraction playbook in arc42 §5.8 applies — provided the enforcement above was maintained. If it was not, the estimate is meaningless.