From Monolith to Microservices, Part 1: The Architecture I Started With
How spice-me evolves from a modular monolith into a microservice architecture — one deliberate step at a time.
From Monolith to Microservices, Part 1: The Architecture I Started With
How spice-me evolves from a modular monolith into a microservice architecture — one deliberate step at a time.
Series roadmap
- This article: the current state — one API, one database, many domains.
- Next article: extracting the identity service and wiring it to the core app.
Introduction
When I set out to build spice-me, I had a clear product goal: a multi-restaurant platform where customers order and reserve tables, restaurant managers run the kitchen, and platform admins own the catalog.
I also had a clear architecture goal: ship fast, keep boundaries clean, and leave room to split services later — without paying the operational cost of microservices on day one.
This article is the first in a series about that evolution. Here I describe what exists today: a modular monolith in a TypeScript monorepo — not a ball of mud, and not yet a distributed system. Understanding this foundation matters, because every future extraction (starting with identity) builds on the module boundaries already drawn in the code.
The application in one paragraph
spice-me is a full-stack food ordering and operations platform. A Next.js frontend talks to a NestJS REST API backed by PostgreSQL. Three roles — platform admin, restaurant admin, and customer — share one backend. Catalog, restaurants, orders, kitchen workflows, tables, reservations, and analytics all live in the same deployable API today.
That’s intentional. The product needed to prove itself as a whole before the infrastructure split.
Why a monolith first
Microservices solve problems you often don’t have at the start: independent scaling per domain, separate team ownership, blast-radius isolation. They introduce problems you always have: distributed transactions, network failures, multiple databases, and harder local development.
I chose a modular monolith instead:
| Approach | What you get |
|---|---|
| Big ball of mud | Fast at first, painful forever |
| Modular monolith ✅ | One deploy, clear module boundaries, refactor-friendly |
| Microservices on day one | Maximum ops overhead before product-market fit |
NestJS encourages the middle path: one module per domain, explicit imports, services that export only what others need. The codebase is structured so a module can become a service later — but nothing is distributed until there’s a reason.
System overview today
Everything the browser needs flows through a single API and a single database.

Stack
| Layer | Technology |
|-------|------------|
| Frontend | Next.js 16, React 19, App Router, Tailwind v4 |
| Backend | NestJS (ESM), Prisma 7, PostgreSQL |
| Auth | Passport JWT — access (15m) + refresh (7d) |
| Tooling | Bun, Turborepo, TypeScript strict |
| API docs | Swagger at `/api/docs` |
Monorepo layout
The repository is a Turborepo workspace — not one giant folder, but not sixteen repositories either.
spice-me/ apps/ api/ ← NestJS backend (the monolith) web/ ← Next.js frontend packages/ typescript-config/ eslint-config/ tailwind-config/
Shared config lives in packages/. Application code stays in apps/. That separation already mirrors how you might share contracts between services later — without running multiple backends yet.
Inside the monolith: modular architecture
AppModule imports every feature module and contains no business logic. Each domain owns its folder:
apps/api/src/ auth/ ← login, JWT, guards users/ ← profiles, admin user CRUD restaurants/ categories/ products/ restaurant-products/ menu/ orders/ restaurant-tables/ ← tables + reservations platform-settings/ stats/ upload/ prisma/ ← global PrismaModule common/ ← shared utilities (e.g. VAT)
Every module follows the same shape:
<feature>/ <feature>.module.ts <feature>.controller.ts ← HTTP only <feature>.service.ts ← business logic dto/ ← validated inputs
Rule: Controller → Service → Prisma. No shortcuts.
How a request moves through the system
Whether the call is public (GET /menu) or protected (POST /orders), the path is the same:

- CORS and the global ValidationPipe run in
main.ts. - Guards validate JWT and role when the route requires it.
- The controller binds the body to a DTO.
- The service applies rules — sometimes calling another module’s exported service.
- Prisma executes SQL against the one database.
Services return profile objects (plain TypeScript types), not raw Prisma entities. That keeps HTTP responses stable even when the schema changes.
Module dependencies: who talks to whom
Most modules are self-contained: they inject PrismaService and touch only their own tables. Cross-module coupling is narrow on purpose — the graph is small enough to draw:

| Exported service | Used by |
|------------------|---------|
| `UsersService` | `AuthModule` |
| `ProductsService` | `MenuModule`, `RestaurantProductsModule`, `OrdersModule` |
| `PlatformSettingsService` | `ProductsModule`, `MenuModule`, `RestaurantProductsModule`, `OrdersModule` |
| `PrismaService` | All modules (global) |
Takeaway for the series: AuthModule + UsersModule form a natural identity boundary. ProductsModule + PlatformSettingsModule form a pricing boundary. Orders orchestrate both but don't own catalog or auth data. Those lines are where future service cuts will land.
Authentication and authorization (still in-process)
Today, identity isn’t a separate service. It’s two modules in the same process:

- JWT payload:
{ sub, email, role }— attached torequest.userasJwtUser. - Global role:
ADMIN,USER,RESTAURANT_ADMIN— enforced byRolesGuard. - Restaurant scope:
RestaurantAdminAssignmentin the database — enforced in services likeOrdersServiceandRestaurantScopeService.
Login, token refresh, and business APIs all hit the same host (localhost:3001). The frontend's api-client refreshes tokens against the same base URL. Simple — and exactly what we'll change first.
Data: one database, logical domains
There’s a single Prisma schema and one PostgreSQL instance. Logically, the data falls into domains that already match modules:

| Domain | Models | Module |
|--------|--------|--------|
| Identity | `User` | `auth`, `users` |
| Restaurants | `Restaurant`, `RestaurantAdminAssignment` | `restaurants` |
| Catalog | `Category`, `Product`, `Ingredient`, … | `products`, `categories`, … |
| Commerce | `Order`, `OrderItem` | `orders` |
| Operations | `RestaurantTable`, `TableReservation` | `restaurant-tables` |
| Platform | `PlatformCommonSettings` | `platform-settings` |
Foreign keys tie Order.userId to User.id today. That coupling is convenient in a monolith — and it's the main friction when identity moves to its own database. Part 2 of this series addresses that directly.
Frontend ↔ API: one backend URL
The web app doesn’t know about microservices yet. It speaks to one API:
- NextAuth credentials provider →
POST /auth/login - Business calls →
NEXT_PUBLIC_API_URL(default[http://localhost:3001)](http://localhost:3001)) - Token refresh →
POST /auth/refreshon the same base URL

Admin and customer UIs are route groups in Next.js ((admin), (app)). Authorization on the server uses the NextAuth session; authorization on the API uses JWT guards. Two layers, one identity source — for now.
What this architecture does well
After building the full product on this stack, a few properties stand out:
- Fast local development.
bun turbo devruns web and API. One database. No service mesh, no port matrix. - Clear module seams. Sixteen feature modules, a small dependency graph, Swagger for every route. New work has an obvious home.
- Transactional consistency. Placing an order can read products, apply VAT, and write snapshots in one database transaction. No sagas required.
- Auth on the hot path is cheap. JWT validation is in-process. No network hop per request.
- The monolith is honest about boundaries. Modules export services sparingly. That discipline is what makes a later split feasible — not wishful thinking.
Where the monolith starts to pinch
I’m not splitting for fashion. There are concrete pressures:
| Pressure | Why it matters |
|----------|----------------|
| **Identity is a distinct capability** | Auth + user CRUD is a cohesive domain; other services will need it without owning user tables |
| **`User` is a hub table** | Orders, reservations, and assignments all FK to `User` — fine in one DB, blocking across services |
| **Security surface** | Login endpoints share fate with order/catalog traffic on one deployable |
| **Team / scale (future)** | Identity could ship on a different cadence than catalog or orders |
These are the reasons the first extraction target is identity — not orders, not catalog. Smallest vertical slice that proves the pattern.
What comes next (Part 2)
The next article in this series covers isolating the identity service:
- Extract
AuthModule+UsersModuleintoapps/identitywith its own database - Keep
apps/apias the core business API (catalog, restaurants, orders, …) - Identity issues JWTs; core validates them locally (no per-request auth hop)
- Service-to-service calls only when core needs user profile data
- Frontend changes: login/refresh → identity; business APIs → core

The detailed migration plan — database split, JWT contract, UserLookupService, phased cutover, and frontend wiring — lives in the repo at docs/plans/identity-service-extraction.md. Part 2 will tell that story as a narrative walkthrough, not just a checklist.
Branch: work begins on micro-main.
Closing
spice-me today is a modular monolith done deliberately: one deploy, one database, sixteen domain modules, and a dependency graph small enough to reason about. That’s not a failure to adopt microservices — it’s the right first architecture for shipping a complex product with a small surface area of operational risk.
The journey to microservices doesn’t start by rewriting everything. It starts by recognizing which module boundary is already drawn in code — and cutting along that line. For spice-me, that line is identity.
Part 1 of the “From Monolith to Microservices” series · spice-me · Bun · Turborepo · NestJS · Next.js
If you’re building something similar and weighing monolith vs. microservices, I’d love to hear how you drew your own boundaries — drop a comment below.
메타데이터
- post_id
- a2b26e707aaa
- slug
- from-monolith-to-microservices-part-1-the-architecture-i-started-with-a2b26e707aaa
- url
- https://medium.com/@ashikshuvo1996/from-monolith-to-microservices-part-1-the-architecture-i-started-with-a2b26e707aaa
- canonical_url
- https://medium.com/@ashikshuvo1996/from-monolith-to-microservices-part-1-the-architecture-i-started-with-a2b26e707aaa
- author_url
- https://medium.com/@ashikshuvo1996
- status
- ok
- fetched_at
- 2026-07-13 06:23:13