Sitemap

6 Common Mistakes in Domain-Driven Design (DDD) with Express.js

5 min readFeb 26, 2025

--

Press enter or click to view image in full size
6 Common Mistakes in Domain-Driven Design (DDD) with Express.js
6 Common Mistakes in Domain-Driven Design (DDD) with Express.js

Domain-Driven Design (DDD) is a game-changer for complex applications, helping developers build software that mirrors real-world business logic. However, implementing DDD with Express.js — one of the most popular Node.js frameworks — comes with its challenges. Many developers, especially those coming from a traditional MVC mindset, fall into common pitfalls when applying DDD principles.

1. Treating Express.js Controllers as the Core of the Application

The Mistake

A common pitfall is placing too much business logic inside Express.js controllers. Many developers treat controllers as the “brain” of their application, leading to bloated, hard-to-maintain files.

Here’s what that typically looks like:

app.post('/order', async (req, res) => {
const { userId, items } = req.body;

// Business logic inside the controller
const totalAmount = items.reduce((sum, item) => sum + item.price, 0);
if (totalAmount < 10) {
return res.status(400).json({ error: 'Minimum order amount is $10' });
}

const order = await OrderModel.create({ userId, items, totalAmount });
res.status(201).json(order);
});

The controller is overloaded with business logic, validation, and database interaction. This tightly couples the framework (Express.js) to the core logic.

How to Fix It

Instead, delegate business logic to domain services and use controllers as a thin layer to handle HTTP requests.

// domain/orderService.js
class OrderService {
createOrder(userId, items) {
const totalAmount = items.reduce((sum, item) => sum + item.price, 0);
if (totalAmount < 10) {
throw new Error('Minimum order amount is $10');
}
return { userId, items, totalAmount };
}
}
export default new OrderService();
// controllers/orderController.js
import OrderService from '../domain/orderService.js';

app.post('/order', async (req, res) => {
try {
const order = OrderService.createOrder(req.body.userId, req.body.items);
res.status(201).json(order);
} catch (error) {
res.status(400).json({ error: error.message });
}
});

Now, the business logic is inside the domain layer, making it reusable and testable.

2. Using Generic Models Instead of Domain Models

The Mistake

Many developers use ORM models (like Mongoose or Sequelize) directly in their services, treating them as domain models.

Example:

const order = await OrderModel.create({ userId, items, totalAmount });

The issue? This makes your business logic tightly coupled to the database schema. If you switch to another database or introduce business logic changes, everything breaks.

How to Fix It

Create explicit domain models that represent business rules and encapsulate logic.

class Order {
constructor(userId, items) {
this.userId = userId;
this.items = items;
this.totalAmount = items.reduce((sum, item) => sum + item.price, 0);

if (this.totalAmount < 10) {
throw new Error('Minimum order amount is $10');
}
}
}

Now, you can use this in your service instead of relying on ORM models directly.

3. Ignoring Ubiquitous Language

The Mistake

DDD emphasizes ubiquitous language, meaning the business terminology should be reflected in the code.

Bad Example:

const newEntry = await Product.create({ name, cost, userId });

The term “newEntry” is vague. Instead, use business language:

const newProduct = await Product.create({ name, price, ownerId });

Using domain-specific terms makes the code more readable and aligns it with business requirements.

4. Over-Complicating Microservices with Premature Bounded Contexts

The Mistake

Some developers break an application into multiple microservices too early, thinking it’s a perfect DDD practice.

Example:

  • A User Service for authentication
  • An Order Service for processing orders
  • A Cart Service for handling carts

While this sounds good, it adds unnecessary complexity — especially if the app is still evolving.

How to Fix It

Start with a monolithic architecture while respecting bounded contexts internally.

Example:

ecommerce-app/
├── domain/
│ ├── user/
│ ├── order/
│ ├── cart/

Later, when needed, you can extract services into microservices without major rewrites.

5. Forgetting to Use Value Objects

The Mistake

Many developers rely on primitive types for important domain values.

Example:

const price = 10.99;
const currency = 'USD';

This leads to inconsistent logic across the application.

How to Fix It

Encapsulate values in Value Objects to enforce consistency.

class Money {
constructor(amount, currency) {
if (amount < 0) throw new Error('Amount must be positive');
if (!['USD', 'EUR', 'GBP'].includes(currency)) throw new Error('Invalid currency');
this.amount = amount;
this.currency = currency;
}
}

Now, use it like this:

const productPrice = new Money(10.99, 'USD');

This prevents invalid values from creeping into your application.

6. Not Handling Domain Events Properly

The Mistake

Many developers use direct function calls instead of event-driven communication.

Example:

OrderService.createOrder(userId, items);
EmailService.sendOrderConfirmation(userId);

Here, OrderService and EmailService are tightly coupled. If you want to notify a warehouse, you have to modify the order service.

How to Fix It

Use domain events and publish-subscribe patterns.

Example with Event Emitters in Node.js:

import EventEmitter from 'events';
const eventBus = new EventEmitter();

// OrderService
eventBus.on('orderCreated', (order) => {
EmailService.sendOrderConfirmation(order.userId);
WarehouseService.processOrder(order);
});

class OrderService {
static createOrder(userId, items) {
const order = new Order(userId, items);
eventBus.emit('orderCreated', order);
return order;
}
}

Now, services are decoupled and can react to events independently.

Final Thoughts

DDD with Express.js requires discipline and a clear separation of concerns. To summarize:

Keep controllers thin — push logic to domain services
Use domain models — avoid treating ORM models as domain logic
Stick to ubiquitous language — name things as business stakeholders do
Don’t overcomplicate bounded contexts — monolith first, microservices later
Use Value Objects — keep data consistent
Embrace domain events — decouple logic using event-driven design

You may also like:

1) 5 Common Mistakes in Backend Optimization

2) 7 Tips for Boosting Your API Performance

3) How to Identify Bottlenecks in Your Backend

4) 8 Tools for Developing Scalable Backend Solutions

5) 5 Key Components of a Scalable Backend System

6) 6 Common Mistakes in Backend Architecture Design

7) 7 Essential Tips for Scalable Backend Architecture

8) Token-Based Authentication: Choosing Between JWT and Paseto for Modern Applications

9) API Rate Limiting and Abuse Prevention Strategies in Node.js for High-Traffic APIs

10) Can You Answer This Senior-Level JavaScript Promise Interview Question?

11) 5 Reasons JWT May Not Be the Best Choice

12) 7 Productivity Hacks I Stole From a Principal Software Engineer

13) 7 Common Mistakes in package.json Configuration

Read more blogs from Here

Share your experiences in the comments, and let’s discuss how to tackle them!

Follow me on Linkedin

--

--

Responses (2)