ANTI-PATTERNS

What NOT to Do

🚨 ANTI-PATTERN WARNING: "COBOL IN MONGODB CLOTHING"

⚠️ The Problem

A common mistake when adopting MongoDB is implementing relational access patterns in a document database, effectively recreating COBOL's sequential file scanning with modern tools.

❌ Example: Permissions System Gone Wrong

The Setup: MongoDB collections: users, files, permissions

Requirement: "Show all files a user can access"

The Anti-Pattern (COBOL-style diving in Node.js)

// Step 1: Scan permissions collection
const permissions = await db.permissions.find({
  userId: "user123"
}).toArray();

// Step 2: Extract file IDs (manual loop logic)
const fileIds = permissions
  .filter(p => p.accessLevel >= 'read')
  .map(p => p.fileId);

// Step 3: Scan files collection
const files = await db.files.find({
  _id: { $in: fileIds }
}).toArray();

// Step 4: Merge in application code
const result = files.map(file => ({
  ...file,
  access: permissions.find(p => p.fileId === file._id)
}));

What you've created:

✗ Must scan multiple collections (like COBOL sequential files)

✗ Manual loop logic in application code

✗ N+1 query patterns

✗ Application-level joins (slow, memory-intensive)

✗ No query optimizer helping you

✓ What SQL Does Better

-- Declarative: database handles optimization
SELECT
  f.*,
  p.access_level,
  p.granted_date
FROM files f
INNER JOIN permissions p ON f.id = p.file_id
WHERE p.user_id = 'user123'
  AND p.access_level >= 'read'
ORDER BY f.created_date DESC;

✓ Single query with declarative intent

✓ Query planner optimizes execution

✓ Indexes on foreign keys

✓ Database does the "diving"

What COBOL Does More Honestly

      * At least COBOL is honest
      * about sequential scanning
       PERFORM VARYING WS-IDX
           FROM 1 BY 1
           UNTIL WS-IDX > MAX-PERMS

           IF PERM-USER-ID(WS-IDX)
                = WS-SEARCH-USER
              AND PERM-ACCESS-LEVEL
                >= 'READ'
              PERFORM GET-FILE-DETAILS
           END-IF
       END-PERFORM.

✓ Explicitly shows you're scanning

✓ No illusion of "modern" efficiency

✓ Procedural logic is expected

💀 THE IRONY 💀

You chose MongoDB to escape relational databases, but ended up:

  1. Normalizing data (separate collections for users, files, permissions)
  2. Manually joining in application code
  3. Sequential scanning like it's 1959
  4. Writing COBOL-style procedural logic in JavaScript

🎯 You've built a slow, distributed COBOL system. 🎯

🎯 The Right Tool for the Job

Use MongoDB when:

  • ✅ Documents are self-contained (blog posts, catalogs)
  • ✅ Schema truly needs to evolve rapidly
  • ✅ You're embedding related data (denormalization)
  • ✅ Horizontal scaling is actually needed

Use SQL when:

  • ✅ Data has many-to-many relationships
  • ← PERMISSIONS SYSTEMS
  • ✅ Complex filtering across normalized tables
  • ✅ Referential integrity is critical

Use COBOL when:

  • ✅ It's 1959 and you're processing payroll
  • ✅ Maintaining a 40-year-old banking system
  • ✅ You want to learn computing history

🔧 How to Fix It

Options from easiest to most drastic:

🔧 Option 1: Fix the Pattern (Work with MongoDB, not against it)

Denormalize - Embed permissions directly:
// Store permissions WITH the user
{
  _id: "user123",
  name: "John Doe",
  accessibleFiles: [
    { fileId: "file1", level: "read", grantedDate: "2024-01-15" },
    { fileId: "file2", level: "write", grantedDate: "2024-02-20" }
  ]
}

// Single query, no joins needed
const user = await db.users.findOne({ _id: "user123" });
const fileIds = user.accessibleFiles.map(f => f.fileId);
Or use aggregation pipeline ($lookup):
db.permissions.aggregate([
  { $match: { userId: "user123", accessLevel: { $gte: "read" } } },
  {
    $lookup: {
      from: "files",
      localField: "fileId",
      foreignField: "_id",
      as: "fileDetails"
    }
  },
  { $unwind: "$fileDetails" }
])
Add proper indexes:
db.permissions.createIndex({ userId: 1, accessLevel: 1 });
db.permissions.createIndex({ fileId: 1 });
db.files.createIndex({ _id: 1 }); // Probably already exists

⚡ Option 2: Add a Caching Layer (Reduce the pain)

// Cache permission lookups in Redis
const cacheKey = `permissions:${userId}`;
let permissions = await redis.get(cacheKey);

if (!permissions) {
  permissions = await db.permissions.find({ userId }).toArray();
  await redis.setex(cacheKey, 3600, JSON.stringify(permissions)); // 1 hour TTL
}

// Invalidate on permission changes
async function grantPermission(userId, fileId, level) {
  await db.permissions.insertOne({ userId, fileId, level });
  await redis.del(`permissions:${userId}`); // Clear cache
}
✓ Benefits:
  • Reduces repeated collection scans
  • Buys time while you fix underlying pattern
  • Relatively low effort to implement
⚠️ Trade-offs:
  • Cache invalidation complexity
  • Another system to manage (Redis)
  • Doesn't fix fundamental design issue

💣 Option 3: Migrate to PostgreSQL (Nuclear option)

-- If the anti-pattern is EVERYWHERE in your codebase
CREATE TABLE permissions (
  user_id VARCHAR(50),
  file_id VARCHAR(50),
  access_level VARCHAR(10),
  granted_date TIMESTAMP,
  PRIMARY KEY (user_id, file_id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (file_id) REFERENCES files(id)
);

-- One query, database-optimized
SELECT f.*, p.access_level
FROM files f
INNER JOIN permissions p ON f.id = p.file_id
WHERE p.user_id = 'user123' AND p.access_level >= 'read';
When to consider this:
  • ✓ Relational data patterns throughout app
  • ✓ Workarounds more expensive than migration
  • ✓ Need strong referential integrity
  • ✓ Team suffering from constant N+1 queries
When NOT to migrate:
  • ✗ This is an isolated problem (fix pattern instead)
  • ✗ Most data IS actually document-oriented
  • ✗ Migration cost > pain of current solution
  • ✗ Team lacks PostgreSQL expertise

⚡ KEY TAKEAWAY ⚡

Technology choices have consequences.

Choosing a database because it's "modern" or "scalable" without understanding your access patterns leads to implementing the worst parts of 1959 technology in a 2009 package.

The Pragmatic Path:

  1. Start with denormalization or indexing (Option 1)
  2. Add caching if you need immediate relief (Option 2)
  3. Only migrate if this anti-pattern is systemic (Option 3)

Don't let your MongoDB become a distributed COBOL mainframe. 💾

📚 EXPLORE MORE

🏠 HOME 📖 INTRODUCTION 💻 EXAMPLES ⚖️ COMPARISON 💎 CONCLUSIONS