What NOT to Do
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.
The Setup: MongoDB collections: users, files, permissions
Requirement: "Show all files a user can access"
// 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
-- 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"
* 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
You chose MongoDB to escape relational databases, but ended up:
🎯 You've built a slow, distributed COBOL system. 🎯
Options from easiest to most drastic:
// 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);
db.permissions.aggregate([
{ $match: { userId: "user123", accessLevel: { $gte: "read" } } },
{
$lookup: {
from: "files",
localField: "fileId",
foreignField: "_id",
as: "fileDetails"
}
},
{ $unwind: "$fileDetails" }
])
db.permissions.createIndex({ userId: 1, accessLevel: 1 });
db.permissions.createIndex({ fileId: 1 });
db.files.createIndex({ _id: 1 }); // Probably already exists
// 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
}
-- 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';
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:
Don't let your MongoDB become a distributed COBOL mainframe. 💾