DEV Community

Cover image for How to structure a Modular Monolith
Agbo, Daniel Onuoha
Agbo, Daniel Onuoha

Posted on

How to structure a Modular Monolith

We’ll build a small e-commerce-like app with three modules:

  • Users → registration & authentication
  • Products → product catalog
  • Orders → order placement

All modules reside within a single Node.js project, but each has distinct boundaries and communicates only through well-defined interfaces.

📂 Project Structure

modular-monolith/
├── package.json
├── src/
│   ├── app.js
│   ├── modules/
│   │   ├── users/
│   │   │   ├── user.controller.js
│   │   │   ├── user.service.js
│   │   │   └── user.model.js
│   │   ├── products/
│   │   │   ├── product.controller.js
│   │   │   ├── product.service.js
│   │   │   └── product.model.js
│   │   └── orders/
│   │       ├── order.controller.js
│   │       ├── order.service.js
│   │       └── order.model.js
│   ├── shared/
│   │   └── database.js
└── server.js
Enter fullscreen mode Exit fullscreen mode

🔑 Shared Setup

src/shared/database.js
Here, we use a simple in-memory database for the demo. In production, each module should own its schema/tables.

// src/shared/database.js
export const db = {
  users: [],
  products: [],
  orders: []
};
Enter fullscreen mode Exit fullscreen mode

👤 Users Module

src/modules/users/user.model.js

export class User {
  constructor({ id, name, email }) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/users/user.service.js

import { db } from "../../shared/database.js";
import { User } from "./user.model.js";

export class UserService {
  createUser(data) {
    const user = new User({ id: Date.now().toString(), ...data });
    db.users.push(user);
    return user;
  }

  getUserById(id) {
    return db.users.find(u => u.id === id);
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/users/user.controller.js

import express from "express";
import { UserService } from "./user.service.js";

const router = express.Router();
const userService = new UserService();

router.post("/", (req, res) => {
  const user = userService.createUser(req.body);
  res.status(201).json(user);
});

router.get("/:id", (req, res) => {
  const user = userService.getUserById(req.params.id);
  if (!user) return res.status(404).json({ message: "User not found" });
  res.json(user);
});

export default router;
Enter fullscreen mode Exit fullscreen mode

📦 Products Module

src/modules/products/product.model.js

export class Product {
  constructor({ id, name, price }) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/products/product.service.js

import { db } from "../../shared/database.js";
import { Product } from "./product.model.js";

export class ProductService {
  addProduct(data) {
    const product = new Product({ id: Date.now().toString(), ...data });
    db.products.push(product);
    return product;
  }

  getProductById(id) {
    return db.products.find(p => p.id === id);
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/products/product.controller.js

import express from "express";
import { ProductService } from "./product.service.js";

const router = express.Router();
const productService = new ProductService();

router.post("/", (req, res) => {
  const product = productService.addProduct(req.body);
  res.status(201).json(product);
});

router.get("/:id", (req, res) => {
  const product = productService.getProductById(req.params.id);
  if (!product) return res.status(404).json({ message: "Product not found" });
  res.json(product);
});

export default router;
Enter fullscreen mode Exit fullscreen mode

🛒 Orders Module (interacts with Users & Products)

src/modules/orders/order.model.js

export class Order {
  constructor({ id, userId, productId }) {
    this.id = id;
    this.userId = userId;
    this.productId = productId;
    this.createdAt = new Date();
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/orders/order.service.js

import { db } from "../../shared/database.js";
import { Order } from "./order.model.js";
import { UserService } from "../users/user.service.js";
import { ProductService } from "../products/product.service.js";

export class OrderService {
  constructor() {
    this.userService = new UserService();
    this.productService = new ProductService();
  }

  placeOrder(userId, productId) {
    const user = this.userService.getUserById(userId);
    const product = this.productService.getProductById(productId);

    if (!user) throw new Error("User not found");
    if (!product) throw new Error("Product not found");

    const order = new Order({ id: Date.now().toString(), userId, productId });
    db.orders.push(order);
    return order;
  }

  listOrders() {
    return db.orders;
  }
}
Enter fullscreen mode Exit fullscreen mode

src/modules/orders/order.controller.js

import express from "express";
import { OrderService } from "./order.service.js";

const router = express.Router();
const orderService = new OrderService();

router.post("/", (req, res) => {
  try {
    const { userId, productId } = req.body;
    const order = orderService.placeOrder(userId, productId);
    res.status(201).json(order);
  } catch (err) {
    res.status(400).json({ message: err.message });
  }
});

router.get("/", (req, res) => {
  res.json(orderService.listOrders());
});

export default router;
Enter fullscreen mode Exit fullscreen mode

🚀 App Setup

src/app.js

import express from "express";
import userRoutes from "./modules/users/user.controller.js";
import productRoutes from "./modules/products/product.controller.js";
import orderRoutes from "./modules/orders/order.controller.js";

const app = express();
app.use(express.json());

// Module routes
app.use("/users", userRoutes);
app.use("/products", productRoutes);
app.use("/orders", orderRoutes);

export default app;
Enter fullscreen mode Exit fullscreen mode

server.js

import app from "./src/app.js";

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Modular Monolith running on http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

✅ How This Reflects a Modular Monolith

  • Single deployable unit → one Node.js process.
  • Modules are isolated → Users, Products, Orders each have their own models, services, controllers.
  • Explicit communication → Orders depends on services, not raw DB access from other modules.
  • Independent ownership → Each module could eventually be extracted into its own microservice if scaling demands.

Top comments (1)

Collapse
 
xwero profile image
david duymelinck

I also think modules are the way to go. I don't like all the unneeded layering of directories.
I even remove the modules directory. Everything is either a module or is in the root.

Of course for more complex projects extra layering can help the discovery.