Architecture العربية

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-sensitive
  • CON-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-01 forces 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.

Tech Lead · Amir Haroun Draft v0.1 · research & design only