Modular monolith vs microservices
One deployable with enforced modules first; move a module out only for a named difference, and pay for every call that then crosses the network.
Predict first
Before you read: predict
Five engineers are starting a ticket-resale product. The founders expect millions of users within two years, but no customer has used it yet, and none of the five has built or run a system of services.
How should version one be built?
The 30-second version
Start as one deployable divided into modules that reach each other only through what each exports; NestJS checks this at startup. Moving a module into its own service lets it deploy and scale alone, and costs network calls, data split across databases and more to operate. Split for a different load, release pace or owning team, and say which.
One deployable with enforced modules first; move a module out only for a named difference, and pay for every call that then crosses the network.
How it works
Step 1 of 15
Analogy: A family department store and its home-delivery department
One building, one set of doors, one opening time.
| At the store | In the system |
|---|---|
| each department serves the others across its own counter; staff from other departments are turned away at its stockroom door | modules reached only through their exports, checked at once (steps 2, 4–5) |
| home delivery's rounds treble before Christmas, it loads vans at five in the morning, and it has its own manager | a different scaling profile, release cadence and owner (step 13) |
| delivery moves to a depot on the ring road | one module moved into its own service (step 8) |
| every parcel handed over is now a van trip, and the depot phones the store to check each address | each call a network request, calls back included (step 9) |
| the evening stock count becomes two counts in two buildings | data owned per service (step 10) |
| two buildings to staff, alarm and insure | the operations premium (step 11) |
In real terms: keep one deployable whose modules meet only at their exports, and move a module out only for a difference you can name.
Breaks down when: a van trip is visibly slow. In code a network call looks like any other method call, so its cost hides.
Contrasting case: a corner shop with one counter and one owner has nothing to wall off and nothing to move out.
Examples, run for real
4.1 ExamHall EXH-2370: a shortcut across a module boundary
EXH-2370: before ExamHall's API moved to NestJS modules, attempts/late.js imported Grading's score repository and wrote late submissions' points straight to the scores table. Grading's own path, record(), applies its late rule first. The script saves five submissions, two of them late:
4.1 EXH-2370 attempts/late.js imports grading/score.repository.js: 5 scores written · 2 skip Grading's late rule (a-4 58, a-5 71)
4.1 MR !2670 late path through GradingService.record(): 5 scores written · 0 skip the late rule (a-4 52, a-5 64) · imports of Grading's internals 0
MR !2670 moves the late path onto GradingService.record() and the code into NestJS modules, where Grading exports only GradingService (steps 2–4). MR !2671 later runs the BullMQ marking workers as their own deployment, because their load after an exam closes differs from the API's (step 13).
4.2 Proof domain, media streaming: one play request before and after a split
A streaming app's play handler calls Rights to check the viewer's licence, then Catalogue for the title's manifest, then signs the playback URL itself. The script runs one play request with every module in one process, then with Rights moved out, then with Rights and Catalogue moved out:
4.2 every module in one process: HTTP requests between services 0 · reply signed:tt-42.m3u8
4.2 Rights moved out: HTTP requests between services 1 (rights.check) · reply signed:tt-42.m3u8
4.2 Rights and Catalogue moved out: HTTP requests between services 2 (rights.check · catalogue.get) · reply signed:tt-42.m3u8
The code is unchanged; each call into a moved module became an HTTP request (step 9).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
ExamHall's API is one NestJS app. A developer wants a late submission's score saved the moment it arrives, so AttemptsService writes it through Grading's repository:
The app is started with NestFactory.createApplicationContext(AppModule), and then one late submission calls saveLate('a-7', 41).
When, if ever, does this fail, and how?
import { Injectable, Module } from '@nestjs/common'; @Injectable() export class ScoreRepository { rows: { attemptId: string; points: number }[] = []; write(attemptId: string, points: number) { this.rows.push({ attemptId, points }); }}@Injectable() export class GradingService { constructor(private scores: ScoreRepository) {} }@Module({ providers: [GradingService, ScoreRepository], exports: [GradingService] })export class GradingModule {} @Injectable() export class AttemptsService { constructor(private scores: ScoreRepository) {} saveLate(attemptId: string, points: number) { this.scores.write(attemptId, points); }}@Module({ imports: [GradingModule], providers: [AttemptsService] })export class AttemptsModule {} @Module({ imports: [AttemptsModule, GradingModule] })export class AppModule {}Question 2 of 5
A shop's checkout is one NestJS app with Orders, Fraud, Stock and Ledger modules. The checkout handler, in Orders, and Ledger's post:
async post(orderId) {
const order = await this.orders.load(orderId);
this.entries.push({ orderId, total: order.total });
}
The team moves Fraud, Stock and Ledger each into its own service, called over HTTP. Orders stays in the app, and each module keeps its code. A customer checks out a basket of 3 items.
After the move, how many HTTP requests between the shop's services does that one checkout cause?
async checkout(orderId, items) { await this.fraud.screen(orderId); for (const item of items) await this.stock.reserve(orderId, item); await this.orders.save(orderId, items); await this.fraud.record(orderId, 'placed'); await this.ledger.post(orderId);}Question 3 of 5
ExamHall's API is one NestJS app with Exams, Attempts, Grading and Proctoring modules, built by one team of six and released together twice a week. CI fails any import of another module's internal files (ESLint no-restricted-imports). Its architecture review receives four proposals to move a module into a service of its own.
Which proposal gives a reason the review should accept?
Question 4 of 5
A video service's team of four plans to move transcoding out of its monolith, because transcoding needs GPU machines and the rest of the app does not. Their readiness notes:
- Servers: a script starts a new GPU server in about an hour.
- Deploys: a pipeline releases transcoding in 15 minutes, after one manual approval.
- Errors: each GPU server writes them to its own log file, which an engineer reads at the weekly review.
- First service: it holds upload, transcoding and packaging together.
- Everything else: every other module stays in the monolith.
Which note should hold the split back until it changes?
Question 5 of 5
A shop's catalog site is one NestJS app:
Startup throws UnknownDependenciesException.
Which one change makes the app start with CartService and SearchService holding the same PriceService instance?
import { Injectable, Module } from '@nestjs/common'; @Injectable() export class PriceService { quote(sku: string) { return sku === 'mug' ? 8 : 12; }}@Module({ providers: [PriceService] })export class PricingModule {} @Injectable() export class CartService { constructor(public prices: PriceService) {} }@Module({ imports: [PricingModule], providers: [CartService] })export class CartModule {} @Injectable() export class SearchService { constructor(public prices: PriceService) {} }@Module({ imports: [PricingModule], providers: [SearchService] })export class SearchModule {} @Module({ imports: [CartModule, SearchModule] })export class AppModule {}Go deeper
Pro tips
- Forbid imports of another module's internal files with a lint rule (for example ESLint's
no-restricted-imports): anewor a direct file import never passes through NestJS's injector, so no module rule sees it. When: reviewing a modular monolith's boundaries (steps 3–4). Verify (RAN,tips.mjs):tip 1 AttemptsService calls new ScoreRepository(): Nest starts · the late score reached Grading's repository: no tip 1 import check, files importing another module's internals: 1 (attempts/late.service.ts -> grading/score.repository)
Source
The Fowler pages are dated essays (2014 and 2015). The NestJS chapter is vendor documentation that changes with releases, so its claims are labelled as of 2026-10-01.
- Martin Fowler, Monolith First (bliki, 3 June 2015), the curriculum row's first source: https://martinfowler.com/bliki/MonolithFirst.html (
fowler-monolith-first.txt). - Martin Fowler, Microservice Premium (bliki, 13 May 2015): https://martinfowler.com/bliki/MicroservicePremium.html (
fowler-microservice-premium.txt), linked from Monolith First's own argument ("a significant MicroservicePremium"). - Martin Fowler, Microservice Prerequisites (bliki, 28 August 2014): https://martinfowler.com/bliki/MicroservicePrerequisites.html (
fowler-microservice-prerequisites.txt), linked from Monolith First ("the MicroservicePrerequisites you need"). - NestJS documentation, Modules (NestJS 12, as of 2026-10-01), the row's third source: https://docs.nestjs.com/modules. That page renders in the browser, and its static HTML holds no chapter text, so the text was read from the page's source file,
content/modules.mdin github.com/nestjs/docs.nestjs.com at commit 79c0884 (22 September 2026), the file's latest commit on 2026-10-01: https://raw.githubusercontent.com/nestjs/docs.nestjs.com/79c0884d1b6c1789263f5ae50bfac101742e2479/content/modules.md (nestjs-modules.txt). P1 and D2 run the behaviour on @nestjs/core and @nestjs/common 12.1.2. - Martin Fowler, Microservice Trade-Offs (1 July 2015): https://martinfowler.com/articles/microservice-trade-offs.html (
fowler-microservice-trade-offs.txt). Teaching only (steps 1, 9 and 10); it keys no item. - The row's second source, Sam Newman's Building Microservices (2nd ed., 2021), is a book that cannot be fetched as text, and keys nothing.
Related lessons
- Read first: Bounded contexts (DDD-lite)
- Related: The database scaling playbook
- Related: Availability math, single points of failure and failover
- Related: Worker threads vs cluster vs horizontal scaling
- Related: Trunk-based development and delivery metrics
- Related: Deployment strategies and rollback
- Related: Partial failure and the ambiguous timeout
Checked against the tools and docs as of 2026-10-01.