Back to all posts

Managing Code Complexity with Abstraction in NestJS

S
sonhp
10 min read

Managing Code Complexity with Abstraction in NestJS

One of the biggest challenges in software development is managing code complexity. As a NestJS application grows, maintaining clean and maintainable code becomes harder. Without proper abstraction, codebases become difficult to read, test, and extend.

In this post, we’ll explore when and how to abstract logic, avoiding over-engineering, and using design patterns effectively.


1. Understanding Code Complexity

Complexity arises when:
✅ Business logic is spread across multiple places
✅ Functions do too many things
✅ Code is hard to change without breaking something else

A common approach to reducing complexity is abstraction, which means separating what code does from how it does it.


2. When and How to Abstract Logic

Good Abstractions: Focus on Reusability & Maintainability

A well-abstracted module:

  • Hides implementation details
  • Provides a clear, easy-to-use API
  • Can be modified without affecting other parts of the system

Bad Abstractions: Over-Engineering

  • Creating too many layers unnecessarily
  • Abstracting things that won’t be reused
  • Making the code harder to follow than the original complexity

Example: Extracting database logic into a repository

Without Abstraction (Mixed Logic in Service)

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  async createUser(data: CreateUserDto): Promise<User> {
    return this.prisma.user.create({ data });
  }

  async findUserByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { email } });
  }
}

With Abstraction (Using a Repository Layer)

@Injectable()
export class UsersRepository {
  constructor(private readonly prisma: PrismaService) {}

  async create(data: CreateUserDto): Promise<User> {
    return this.prisma.user.create({ data });
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { email } });
  }
}

@Injectable()
export class UsersService {
  constructor(private readonly usersRepo: UsersRepository) {}

  async registerUser(data: CreateUserDto): Promise<User> {
    return this.usersRepo.create(data);
  }
}

Why is this better?
✅ The service focuses on business logic, not database operations.
✅ The repository isolates data access, making it easier to replace Prisma if needed.


3. Avoiding Over-Engineering

While abstraction helps manage complexity, too much abstraction can make code unreadable and unnecessary.

Bad Example: Over-Abstraction

@Injectable()
export class GenericRepository<T> {
  constructor(private readonly prisma: PrismaService) {}

  async create(data: T): Promise<T> {
    return this.prisma.model.create({ data });
  }
}
  • This generic repository might seem flexible, but it introduces more complexity than benefits.
  • Every model might need custom queries, making this abstraction harder to maintain.

Instead, create simple repositories for each entity as needed.


4. Using Design Patterns in NestJS

Design patterns help us write maintainable code while avoiding rigid and overly complex architectures. Let’s look at three useful patterns:

🏗️ 1. Factory Pattern (Encapsulating Object Creation)

The Factory Pattern is useful when you need a clean way to create instances of objects.

Example: Creating Dynamic User Objects

class UserFactory {
  static createAdmin(email: string): User {
    return new User(email, 'admin');
  }

  static createCustomer(email: string): User {
    return new User(email, 'customer');
  }
}

// Usage:
const admin = UserFactory.createAdmin('admin@example.com');
const customer = UserFactory.createCustomer('customer@example.com');

Why use this?
Encapsulates object creation
Prevents redundant code across services


🎭 2. Strategy Pattern (Replacing Conditionals with Pluggable Strategies)

The Strategy Pattern is useful when you need different behaviors for similar operations.

Example: Payment Processing Strategies

interface PaymentStrategy {
  pay(amount: number): void;
}

class PayPalPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid ${amount} using PayPal`);
  }
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number) {
    console.log(`Paid ${amount} using Credit Card`);
  }
}

class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}

  process(amount: number) {
    this.strategy.pay(amount);
  }
}

// Usage:
const paypalProcessor = new PaymentProcessor(new PayPalPayment());
paypalProcessor.process(100);

Why use this?
Removes if-else conditions in payment processing
Allows easy extension (add new payment methods without modifying existing code)


🔄 3. Decorator Pattern (Adding Behavior Dynamically)

The Decorator Pattern is useful for adding behavior to a class dynamically.

Example: Logging Decorator for Services

function LogExecution() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      console.log(`Executing ${key} with args:`, args);
      const result = await originalMethod.apply(this, args);
      console.log(`Result:`, result);
      return result;
    };
  };
}

class OrderService {
  @LogExecution()
  async placeOrder(orderId: number) {
    return `Order ${orderId} placed successfully!`;
  }
}

// Usage:
const service = new OrderService();
service.placeOrder(123);

Why use this?
Adds logging behavior without modifying the original method
Keeps code clean & modular


5. Summary

Best Practices for Managing Code Complexity in NestJS

  • Abstract logic properly – Hide implementation details but don’t overdo it.
  • Use a repository layer – Keep services focused on business logic.
  • Avoid unnecessary abstractions – Keep code simple where possible.
  • Leverage design patterns – Use Factory, Strategy, and Decorator patterns to organize logic.

What strategies do you use to manage complexity in your NestJS projects? Let’s discuss in the comments!

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles