Skip to content
Bitloops - Give your AI agents high-signal context in milliseconds.
HomeAbout us
DocsBlog
ResourcesSoftware DesignAnti-Patterns in Software Design: What NOT to Do

Anti-Patterns in Software Design: What NOT to Do

Anti-patterns are solutions that seem smart but create more problems than they solve. Recognize them early—God Objects, Anemic Models, Big Ball of Mud, Spaghetti Code—and you'll save yourself weeks of debugging and refactoring later.

9 min readUpdated March 4, 2026Software Design

What Anti-Patterns Are

Anti-patterns are solutions that seem clever but create more problems than they solve. They're the dark side of software design — so common they have names. Recognizing them early saves enormous pain.

God Object

The Problem: One class does everything.

// God Object: Order knows about everything
class Order {
  // Core responsibility: order logic
  constructor(id, customer, items) {
    this.id = id;
    this.customer = customer;
    this.items = items;
  }

  placeOrder() { /* ... */ }
  calculateTotal() { /* ... */ }

  // Persistence (shouldn't be here)
  save() {
    database.insert('orders', this.toJSON());
  }

  // Notifications (shouldn't be here)
  sendConfirmationEmail() {
    emailService.send(this.customer.email, 'Order Confirmed');
  }

  // Reporting (shouldn't be here)
  generateInvoice() {
    return new Invoice(this.id, this.customer, this.items);
  }

  // Payments (shouldn't be here)
  processPayment(paymentInfo) {
    paymentService.charge(paymentInfo, this.getTotal());
  }

  // Shipping (shouldn't be here)
  scheduleShipment() {
    shippingService.schedule(this.id);
  }

  // Dozens more...
}
javascript

Why It Happens: It starts innocent. "Just add this one thing to Order." Then another. Then another. Before you realize it, Order is a thousand-line monster.

Why It's Bad:

  • Hard to test (needs everything: database, email, payment service)
  • Hard to modify (change one thing, break three others)
  • Hard to understand (what does Order actually do?)
  • Hard to reuse (can't use Order without all its dependencies)

How to Fix It: Extract responsibilities into separate classes.

// Fixed: Clear separation of concerns
class Order {
  constructor(id, customer, items) {
    this.id = id;
    this.customer = customer;
    this.items = items;
  }

  placeOrder() { /* order logic only */ }
  calculateTotal() { /* calculation only */ }
}

class OrderRepository {
  save(order) {
    database.insert('orders', order.toJSON());
  }
}

class OrderNotificationService {
  sendConfirmation(order) {
    emailService.send(order.customer.email, 'Order Confirmed');
  }
}

class InvoiceGenerator {
  generate(order) {
    return new Invoice(order.id, order.customer, order.items);
  }
}

class OrderPaymentProcessor {
  process(order, paymentInfo) {
    paymentService.charge(paymentInfo, order.getTotal());
  }
}

// Usage
const order = new Order(1, customer, items);
order.placeOrder();
orderRepository.save(order);
notificationService.sendConfirmation(order);
javascript

Order now has one responsibility: managing order data and logic. Everything else is delegated.

Anemic Domain Model

The Problem: Objects have no behavior, only data.

// Anemic: Order is just a data container
class Order {
  constructor(id, customerId, items, status) {
    this.id = id;
    this.customerId = customerId;
    this.items = items;
    this.status = status;
  }
}

// All logic lives in services
class OrderService {
  placeOrder(order) {
    if (order.items.length === 0) {
      throw new Error("Order must have items");
    }
    if (order.total > this.getCustomerCreditLimit(order.customerId)) {
      throw new Error("Order exceeds credit limit");
    }
    order.status = 'placed'; // Mutating the anemic object
  }

  calculateTotal(order) {
    return order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }
}
javascript

Why It Happens: It's easy. Separate data from logic. But it violates the fundamental principle of object-oriented programming — and it's the exact opposite of what Domain-Driven Design advocates.

Why It's Bad:

  • Business logic is scattered across many services
  • Objects don't defend their invariants (anyone can mutate them)
  • Hard to understand what an Order actually does
  • Testing requires mocking dozens of services

How to Fix It: Move behavior into the object.

// Rich domain model: Order knows how to validate itself
class Order {
  constructor(id, customerId, items) {
    this.id = id;
    this.customerId = customerId;
    this.items = items;
    this.status = 'created';
  }

  addItem(product, quantity) {
    this.items.push({ product, quantity });
  }

  canPlace(creditLimit) {
    return this.getTotal() <= creditLimit;
  }

  place(creditLimit) {
    if (this.items.length === 0) {
      throw new Error("Order must have items");
    }
    if (!this.canPlace(creditLimit)) {
      throw new Error("Order exceeds credit limit");
    }
    this.status = 'placed';
  }

  getTotal() {
    return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }
}

// Service is thin: just orchestration
class OrderService {
  placeOrder(order, customer) {
    order.place(customer.creditLimit);
    this.repository.save(order);
  }
}
javascript

Now Order encapsulates its logic. It's testable, understandable, and self-defending.

Big Ball of Mud

The Problem: No clear structure. Everything is tangled together.

// Spaghetti: Hard to trace what depends on what
function processUserRegistration(userData) {
  // Database call
  const connection = mysql.createConnection(config);
  const user = connection.query(`SELECT * FROM users WHERE email = ?`, [userData.email]);

  // Email validation
  if (!userData.email.includes('@')) return error;

  // Password hashing
  const hashedPassword = crypto.hash(userData.password);

  // Database insertion
  connection.query(`INSERT INTO users VALUES (?, ?)`, [userData.email, hashedPassword]);

  // Send email
  nodemailer.send({
    to: userData.email,
    subject: 'Welcome',
    body: `Welcome ${userData.name}`
  });

  // Update cache
  redis.set(`user:${userData.email}`, userData);

  // Log analytics
  analytics.track('user_registered', { email: userData.email });

  // Call external API
  const response = fetch(`https://external-api.com/notify`, {
    email: userData.email,
    timestamp: Date.now()
  });

  return response;
}
javascript

Everything mixed together. Hard to test. Hard to change. Hard to understand.

How to Fix It: Create clear structure and separation of concerns.

class UserRegistration {
  constructor(repository, emailService, validator) {
    this.repository = repository;
    this.emailService = emailService;
    this.validator = validator;
  }

  register(userData) {
    // Validate
    this.validator.validate(userData);

    // Create user
    const user = new User(userData.email, userData.name, userData.password);

    // Persist
    this.repository.save(user);

    // Notify
    this.emailService.sendWelcome(user);

    // Publish event for other systems
    eventBus.publish(new UserRegisteredEvent(user));

    return user;
  }
}

// Each responsibility is clear and testable
class UserValidator {
  validate(userData) {
    if (!userData.email.includes('@')) throw new Error('Invalid email');
  }
}

class UserRepository {
  save(user) {
    database.insert('users', user.toJSON());
  }
}

class WelcomeEmailService {
  sendWelcome(user) {
    emailService.send(user.email, 'Welcome', `Welcome ${user.name}`);
  }
}
javascript

Clear structure. Each class has one job. Easy to test, change, and understand.

Spaghetti Code

The Problem: Tangled control flow. Hard to follow.

function processOrder(order) {
  if (order.items.length > 0) {
    if (order.customer.isActive) {
      if (order.total < order.customer.creditLimit) {
        order.status = 'processing';
        if (paymentService.charge(order.customer.id, order.total)) {
          inventory.allocate(order.items);
          if (shipping.canShip(order.items)) {
            shipping.schedule(order.id);
            notification.send(order.customer.email, 'Order confirmed');
            if (order.customer.loyaltyPoints > 100) {
              rewards.applyDiscount(order.customer.id, 10);
            }
          } else {
            order.status = 'waiting_for_stock';
          }
        } else {
          order.status = 'payment_failed';
        }
      } else {
        throw new Error('Credit limit exceeded');
      }
    } else {
      throw new Error('Customer account inactive');
    }
  } else {
    throw new Error('Order is empty');
  }
}
javascript

Nested ifs. Hard to follow. Easy to mess up.

How to Fix It: Use early returns (guard clauses).

function processOrder(order) {
  // Guard clauses: fail fast
  if (order.items.length === 0) {
    throw new Error('Order is empty');
  }

  if (!order.customer.isActive) {
    throw new Error('Customer account inactive');
  }

  if (order.total > order.customer.creditLimit) {
    throw new Error('Credit limit exceeded');
  }

  // Now happy path is clear
  order.status = 'processing';

  const charged = paymentService.charge(order.customer.id, order.total);
  if (!charged) {
    order.status = 'payment_failed';
    return;
  }

  inventory.allocate(order.items);

  if (!shipping.canShip(order.items)) {
    order.status = 'waiting_for_stock';
    return;
  }

  shipping.schedule(order.id);
  notification.send(order.customer.email, 'Order confirmed');

  if (order.customer.loyaltyPoints > 100) {
    rewards.applyDiscount(order.customer.id, 10);
  }
}
javascript

Much clearer. Guard clauses at the top, happy path in the middle.

Golden Hammer

The Problem: Using the same solution for every problem.

// "We built this with REST APIs, so let's use REST for everything"
// Including real-time notifications that need bidirectional communication
// Including analytics that should be asynchronous
// Including heavy batch processing that should be events-driven

// Or: "We use Kubernetes for everything"
// Including the simple internal admin tool
// Including the one-off data migration script
// Including the scheduled report generator

// Or: "Everything is a microservice"
// Including tightly-coupled business logic that should be in one context
// Including data that should be transactionally consistent
javascript

Using a technology because you like it, not because it solves the problem. The classic example: forcing microservices on a system that would be better served by a modular monolith.

How to Fix It: Choose the right tool for each problem. Use architectural tradeoff frameworks instead of hype.

// APIs: good for synchronous request-response
class OrderAPI {
  post('/orders', (req, res) => {
    // Create order synchronously
  });
}

// Message queue: good for asynchronous events
eventBus.publish(new OrderPlacedEvent(...));

class InventoryService {
  onOrderPlaced(event) {
    // Allocate stock asynchronously
  }
}

// Scheduled jobs: good for batch processing
scheduler.schedule('0 2 * * *', () => {
  generateDailyReport();
});

// Monolith: good for tightly-coupled logic
class OrderContext {
  // Order, Payment, Customer all in same transaction
}
javascript

Different tools for different jobs.

Premature Optimization

The Problem: Optimizing before you know it's needed.

// Premature optimization: Complex caching nobody needs
class UserService {
  constructor() {
    this.cache = new LRUCache(1000);
    this.cacheHits = 0;
    this.cacheMisses = 0;
  }

  getUser(id) {
    const key = `user:${id}`;
    if (this.cache.has(key)) {
      this.cacheHits++;
      return this.cache.get(key);
    }

    this.cacheMisses++;
    const user = database.query(`SELECT * FROM users WHERE id = ?`, [id]);
    this.cache.set(key, user);
    return user;
  }

  clearCache() { /* ... */ }
  invalidateUserCache(id) { /* ... */ }
  // ...
}

// Turns out: users are loaded rarely
// Cache adds complexity without benefit
javascript

How to Fix It: Make it work first. Optimize only if profiling shows a problem.

// Start simple
class UserService {
  getUser(id) {
    return database.query(`SELECT * FROM users WHERE id = ?`, [id]);
  }
}

// Later, if profiling shows performance issues:
// "90% of requests are for the same 10 users"
// THEN add caching, with confidence it's needed
javascript

Cargo Cult Programming

The Problem: Doing something because everyone else does, without understanding why.

// Cargo cult: "Everyone uses Docker, so we put everything in Docker"
// Including local development scripts that run once

// "Everyone uses microservices, so let's split our monolith into 50 services"
// Creating more coordination problems than we solved

// "Everyone tests everything, so we test getters and setters"
// Spending time on low-value tests

// "Everyone uses dependency injection, so we DI everything"
// Including things that don't need flexibility

class Logger {
  constructor(injectedConsole = console) {
    this.console = injectedConsole;
  }
  // Pointless DI. Logger will never change.
}
javascript

Following practices without understanding the reasoning.

How to Fix It: Understand the why. Apply thoughtfully.

// Use Docker where it matters: production consistency, dependency isolation
// Use microservices where there's genuine independent scaling or team ownership
// Test what matters: business logic, complex behavior, edge cases
// Use DI where flexibility actually helps: external dependencies, things that change

class PaymentProcessor {
  constructor(paymentGateway) {
    // Makes sense: different payment gateways in different environments
    this.gateway = paymentGateway;
  }
}

class OrderCalculator {
  // No DI needed: this logic never changes
  calculateTotal(items, taxRate) {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0) * (1 + taxRate);
  }
}
javascript

How AI Introduces Anti-Patterns

AI code generation is particularly prone to certain anti-patterns:

Anemic Models: AI defaults to data classes with service layers because it's predictable.

God Objects: Without clear boundaries, AI accumulates functionality in one class.

Copy-Paste Code: AI generates similar code for different cases instead of extracting patterns.

Over-Engineering: AI adds abstractions for flexibility that never materializes.

At Bitloops, we combat these by encoding design patterns and principles into code generation, so AI-generated code starts with good design.

FAQ

How do I know if I have an anti-pattern?

Warning signs: hard to test, hard to change, confusing to read, mixing many responsibilities, duplicated logic.

Should I refactor to remove anti-patterns immediately?

Only if they're causing pain. Small anti-patterns in low-risk code are fine. Major anti-patterns in critical paths should be fixed.

Can patterns become anti-patterns?

Yes. A pattern is right for its context. Use it in the wrong context and it becomes an anti-pattern. MVC works for web apps. It's overkill for a script.

How do I convince my team to fix anti-patterns?

Show concrete costs: "Changing this takes a week because the God Object touches everything." "Tests fail randomly because of spaghetti code."

Primary Sources

  • Definitive pattern language covering Big Ball of Mud and other architectural anti-patterns from 1997. Big Ball of Mud
  • Microsoft's guide to software anti-patterns, their causes, consequences, and solutions. Anti-Patterns
  • Martin Fowler's comprehensive reference for refactoring techniques and code smell recognition. Refactoring
  • Domain-driven design fundamentals for modeling complex business domains correctly. Domain-Driven Design
  • Practical patterns for enterprise application architecture and distributed systems design. EAA Patterns
  • Kent Beck's foundational guide to test-driven development methodology. TDD by Example

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash