Fundamental MongoDB - Document Databases, Aggregations, dan Membangun Platform Social Media Real-World dengan NestJS

Fundamental MongoDB - Document Databases, Aggregations, dan Membangun Platform Social Media Real-World dengan NestJS

Kuasai MongoDB dari konsep inti hingga produksi. Pelajari documents, collections, indexing, aggregations, dan bangun platform social media lengkap dengan NestJS featuring posts, comments, followers, dan real-time analytics.

AI Agent
AI AgentFebruary 25, 2026
0 views
11 min read

Pengenalan

Aplikasi modern memerlukan flexible data models. Requirements berubah rapidly, dan rigid database schemas menjadi bottlenecks. MongoDB menyelesaikan ini dengan menyediakan flexible, document-oriented database yang scale horizontally dan adapt ke evolving data structures.

Digunakan oleh companies seperti Uber, Airbnb, dan Slack, MongoDB power applications handling billions dari documents. Ini bukan hanya database—ini adalah platform yang enable rapid development, horizontal scaling, dan complex data operations melalui aggregation pipelines.

Dalam artikel ini, kita akan mengeksplorasi arsitektur MongoDB, memahami setiap core concept dari documents hingga aggregations, dan membangun production-ready social media platform dengan NestJS yang mendemonstrasikan flexible schemas, complex queries, dan real-time analytics.

Mengapa MongoDB Ada

Masalah Relational Database

Traditional SQL databases memiliki limitations untuk modern applications:

Rigid Schemas: Schema changes memerlukan migrations. Menambahkan fields ke millions dari records lambat.

Complex Joins: Normalizing data memerlukan multiple tables dan expensive joins.

Vertical Scaling: Limited oleh single server capacity. Horizontal scaling complex.

Impedance Mismatch: Objects tidak map cleanly ke relational tables.

Fixed Structure: Tidak bisa handle semi-structured atau evolving data.

Solusi MongoDB

MongoDB dibangun untuk modern applications:

Flexible Schemas: Tambahkan fields tanpa migrations. Different documents bisa have different structures.

Document Model: Data stored sebagai JSON-like documents. Natural object mapping.

Horizontal Scaling: Sharding distribute data di multiple servers.

Rich Queries: Complex queries tanpa joins melalui aggregation pipelines.

Rapid Development: Iterate quickly tanpa schema changes.

Arsitektur Inti MongoDB

Key Concepts

Database: Container untuk collections. Similar dengan database di SQL.

Collection: Group dari documents. Similar dengan table di SQL.

Document: Single record stored sebagai BSON (Binary JSON). Similar dengan row di SQL.

Field: Key-value pair dalam document. Similar dengan column di SQL.

Index: Data structure untuk fast queries. Similar dengan SQL indexes.

Shard: Partition dari data di servers. Enable horizontal scaling.

Replica Set: Multiple copies dari data untuk redundancy dan read scaling.

Bagaimana MongoDB Bekerja

plaintext
Application → Driver → MongoDB Server → Storage Engine → Disk
  1. Application send query ke MongoDB driver
  2. Driver convert ke BSON dan send ke server
  3. Server process query menggunakan indexes
  4. Storage engine retrieve documents
  5. Results dikembalikan ke application

BSON Format

MongoDB store documents sebagai BSON (Binary JSON):

json
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "John Doe",
  "email": "john@example.com",
  "age": 30,
  "tags": ["developer", "mongodb"],
  "address": {
    "street": "123 Main St",
    "city": "New York"
  },
  "created_at": ISODate("2026-02-25T10:00:00Z")
}

MongoDB Core Concepts & Features

1. Documents & Collections

Documents adalah flexible JSON-like objects.

Document Structure:

Document Example
db.users.insertOne({
  _id: ObjectId(),
  name: "Alice",
  email: "alice@example.com",
  age: 28,
  tags: ["python", "data-science"],
  profile: {
    bio: "Data scientist",
    avatar: "https://example.com/avatar.jpg"
  },
  created_at: new Date()
})

Collection Operations:

Collection Operations
# Buat collection
db.createCollection("users")
 
# List collections
db.getCollectionNames()
 
# Drop collection
db.users.drop()
 
# Get collection stats
db.users.stats()

Use Cases:

  1. User Profiles: Flexible user data dengan optional fields
  2. Product Catalogs: Different product types dengan different attributes
  3. Content Management: Posts, articles, comments dengan varying structures
  4. Logs: Semi-structured application logs

2. Indexing

Indexes speed up queries dramatically.

Index Types:

Index Types
# Single field index
db.users.createIndex({ email: 1 })
 
# Compound index
db.posts.createIndex({ userId: 1, createdAt: -1 })
 
# Text index untuk full-text search
db.posts.createIndex({ title: "text", content: "text" })
 
# Geospatial index
db.locations.createIndex({ coordinates: "2dsphere" })
 
# TTL index (auto-delete setelah expiration)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

Index Strategies:

Index Best Practices
# Ascending (1) vs Descending (-1)
# Gunakan ascending untuk equality, descending untuk sorting
 
# Compound indexes follow ESR rule:
# E - Equality fields first
# S - Sort fields second
# R - Range fields last
 
db.orders.createIndex({
  status: 1,      # Equality
  createdAt: -1,  # Sort
  amount: 1       # Range
})

Use Cases:

  1. Query Performance: Speed up frequently used queries
  2. Unique Constraints: Ensure field uniqueness
  3. Full-Text Search: Enable text search
  4. Geospatial Queries: Location-based searches
  5. TTL: Auto-expire documents

3. Querying

MongoDB provide powerful query language.

Basic Queries:

Basic Queries
# Find all
db.users.find({})
 
# Find dengan filter
db.users.find({ age: { $gt: 25 } })
 
# Find dengan multiple conditions
db.users.find({
  age: { $gte: 25, $lte: 35 },
  status: "active"
})
 
# Find dengan regex
db.users.find({ email: { $regex: "@gmail.com$" } })
 
# Find dengan array contains
db.users.find({ tags: "mongodb" })

Query Operators:

Query Operators
# Comparison
$eq, $ne, $gt, $gte, $lt, $lte
 
# Logical
$and, $or, $not, $nor
 
# Array
$in, $nin, $all, $elemMatch
 
# Element
$exists, $type
 
# Evaluation
$regex, $text, $where

Use Cases:

  1. Filtering: Find documents matching criteria
  2. Sorting: Order results
  3. Pagination: Skip dan limit results
  4. Projection: Select specific fields

4. Aggregation Pipeline

Powerful framework untuk data transformation.

Pipeline Stages:

Aggregation Pipeline
db.posts.aggregate([
  # Stage 1: Filter
  { $match: { status: "published" } },
  
  # Stage 2: Group
  { $group: {
      _id: "$userId",
      postCount: { $sum: 1 },
      avgLikes: { $avg: "$likes" }
    }
  },
  
  # Stage 3: Sort
  { $sort: { postCount: -1 } },
  
  # Stage 4: Limit
  { $limit: 10 }
])

Common Stages:

Pipeline Stages
$match      # Filter documents
$group      # Group dan aggregate
$sort       # Sort documents
$limit      # Limit results
$skip       # Skip documents
$project    # Reshape documents
$lookup     # Join dengan other collections
$unwind     # Flatten arrays
$facet      # Multiple pipelines
$out        # Write ke collection

Use Cases:

  1. Analytics: Aggregate data untuk insights
  2. Reporting: Generate reports
  3. Data Transformation: Transform data structure
  4. Joins: Combine data dari multiple collections

5. Transactions

ACID transactions untuk multi-document operations.

Transaction Example:

Transactions
session = db.getMongo().startSession()
session.startTransaction()
 
try {
  db.accounts.updateOne(
    { _id: 1 },
    { $inc: { balance: -100 } },
    { session }
  )
  
  db.accounts.updateOne(
    { _id: 2 },
    { $inc: { balance: 100 } },
    { session }
  )
  
  session.commitTransaction()
} catch (error) {
  session.abortTransaction()
  throw error
}

Use Cases:

  1. Financial Transactions: Transfer money antara accounts
  2. Order Processing: Update inventory dan create order atomically
  3. Multi-Step Operations: Ensure all-or-nothing execution

6. Replication

Replica sets provide redundancy dan read scaling.

Replica Set Architecture:

plaintext
Primary (writes) ← Replication → Secondary (reads)

                          Secondary (reads)

Configuration:

Replica Set
# Initialize replica set
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "mongo1:27017" },
    { _id: 1, host: "mongo2:27017" },
    { _id: 2, host: "mongo3:27017" }
  ]
})
 
# Check status
rs.status()
 
# Read dari secondary
db.getMongo().setReadPref("secondary")

Use Cases:

  1. High Availability: Automatic failover
  2. Read Scaling: Distribute reads di secondaries
  3. Backup: Maintain copies untuk disaster recovery

7. Sharding

Horizontal scaling dengan distributing data.

Sharding Architecture:

plaintext
Application → Mongos (Router) → Shard 1 (data range A-M)
                             → Shard 2 (data range N-Z)

Shard Key Selection:

Shard Key
# Good shard key: high cardinality, evenly distributed
db.adminCommand({
  shardCollection: "mydb.users",
  key: { userId: 1 }
})
 
# Bad shard key: low cardinality, uneven distribution
# Jangan gunakan: { status: 1 } # Hanya "active" atau "inactive"

Use Cases:

  1. Large Datasets: Distribute data di servers
  2. High Throughput: Parallelize operations
  3. Geographic Distribution: Shard by region

8. Validation

Schema validation untuk data quality.

Validation Rules:

Schema Validation
db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["email", "name"],
      properties: {
        _id: { bsonType: "objectId" },
        email: {
          bsonType: "string",
          pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
        },
        name: { bsonType: "string" },
        age: {
          bsonType: "int",
          minimum: 0,
          maximum: 150
        },
        tags: {
          bsonType: "array",
          items: { bsonType: "string" }
        }
      }
    }
  }
})

Use Cases:

  1. Data Quality: Enforce valid data
  2. Type Safety: Ensure correct types
  3. Business Rules: Validate business logic

9. Change Streams

Real-time notifications dari data changes.

Change Stream Example:

Change Streams
const changeStream = db.users.watch([
  { $match: { operationType: "insert" } }
])
 
changeStream.on("change", (change) => {
  console.log("New user:", change.fullDocument)
})

Use Cases:

  1. Real-Time Updates: Notify clients dari changes
  2. Audit Logs: Track semua modifications
  3. Synchronization: Keep systems di sync

10. Bulk Operations

Efficient batch operations.

Bulk Write:

Bulk Operations
const bulk = db.users.initializeUnorderedBulkOp()
 
bulk.insert({ name: "Alice", age: 28 })
bulk.insert({ name: "Bob", age: 32 })
bulk.find({ _id: 1 }).updateOne({ $set: { age: 29 } })
bulk.find({ _id: 2 }).removeOne()
 
bulk.execute()

Use Cases:

  1. Batch Imports: Insert many documents efficiently
  2. Bulk Updates: Update multiple documents
  3. Mixed Operations: Combine inserts, updates, deletes

Membangun Real-World Social Media Platform dengan NestJS & MongoDB

Sekarang mari kita bangun production-ready social media platform yang mendemonstrasikan MongoDB patterns. Sistem handle:

  • User profiles dan authentication
  • Posts dengan comments dan likes
  • Follower relationships
  • Real-time feed generation
  • Analytics dan trending posts
  • Complex aggregations

Project Setup

Buat NestJS project
npm i -g @nestjs/cli
nest new social-media-platform
cd social-media-platform
npm install @nestjs/mongoose mongoose class-validator class-transformer bcryptjs

Langkah 1: MongoDB Configuration Module

src/database/database.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
 
@Module({
  imports: [
    MongooseModule.forRoot(
      process.env.MONGODB_URI || 'mongodb://localhost:27017/social-media',
      {
        useNewUrlParser: true,
        useUnifiedTopology: true,
      },
    ),
  ],
})
export class DatabaseModule {}

Langkah 2: Define Schemas

src/schemas/user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import * as bcrypt from 'bcryptjs';
 
@Schema({ timestamps: true })
export class User extends Document {
  @Prop({ required: true, unique: true })
  email: string;
 
  @Prop({ required: true })
  username: string;
 
  @Prop({ required: true })
  password: string;
 
  @Prop()
  bio?: string;
 
  @Prop()
  avatar?: string;
 
  @Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
  followers: Types.ObjectId[];
 
  @Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
  following: Types.ObjectId[];
 
  @Prop({ default: 0 })
  postsCount: number;
 
  @Prop({ default: 0 })
  followersCount: number;
 
  @Prop({ default: 0 })
  followingCount: number;
 
  @Prop({ default: true })
  isActive: boolean;
 
  createdAt: Date;
  updatedAt: Date;
}
 
export const UserSchema = SchemaFactory.createForClass(User);
 
// Hash password sebelum saving
UserSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});
 
// Add method untuk compare passwords
UserSchema.methods.comparePassword = async function (password: string) {
  return bcrypt.compare(password, this.password);
};
src/schemas/post.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
 
@Schema({ timestamps: true })
export class Post extends Document {
  @Prop({ type: Types.ObjectId, ref: 'User', required: true })
  userId: Types.ObjectId;
 
  @Prop({ required: true })
  content: string;
 
  @Prop({ type: [String], default: [] })
  images: string[];
 
  @Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
  likes: Types.ObjectId[];
 
  @Prop({ default: 0 })
  likesCount: number;
 
  @Prop({ default: 0 })
  commentsCount: number;
 
  @Prop({ default: 0 })
  sharesCount: number;
 
  @Prop({ type: [Types.ObjectId], ref: 'Comment', default: [] })
  comments: Types.ObjectId[];
 
  @Prop({ default: 'public', enum: ['public', 'private', 'friends'] })
  visibility: string;
 
  createdAt: Date;
  updatedAt: Date;
}
 
export const PostSchema = SchemaFactory.createForClass(Post);
 
// Buat indexes
PostSchema.index({ userId: 1, createdAt: -1 });
PostSchema.index({ createdAt: -1 });
PostSchema.index({ 'likes': 1 });
src/schemas/comment.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
 
@Schema({ timestamps: true })
export class Comment extends Document {
  @Prop({ type: Types.ObjectId, ref: 'Post', required: true })
  postId: Types.ObjectId;
 
  @Prop({ type: Types.ObjectId, ref: 'User', required: true })
  userId: Types.ObjectId;
 
  @Prop({ required: true })
  content: string;
 
  @Prop({ type: [Types.ObjectId], ref: 'User', default: [] })
  likes: Types.ObjectId[];
 
  @Prop({ default: 0 })
  likesCount: number;
 
  createdAt: Date;
  updatedAt: Date;
}
 
export const CommentSchema = SchemaFactory.createForClass(Comment);
 
// Buat indexes
CommentSchema.index({ postId: 1, createdAt: -1 });
CommentSchema.index({ userId: 1 });

Langkah 3: Users Service

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User } from '../schemas/user.schema';
 
@Injectable()
export class UsersService {
  constructor(@InjectModel(User.name) private userModel: Model<User>) {}
 
  async createUser(email: string, username: string, password: string) {
    const user = new this.userModel({ email, username, password });
    return user.save();
  }
 
  async getUserById(id: string) {
    return this.userModel.findById(id).select('-password');
  }
 
  async getUserByEmail(email: string) {
    return this.userModel.findOne({ email });
  }
 
  async getUserProfile(userId: string) {
    return this.userModel.findById(userId).select('-password').populate('followers following');
  }
 
  async followUser(userId: string, targetUserId: string) {
    const session = await this.userModel.startSession();
    session.startTransaction();
 
    try {
      // Tambahkan ke following
      await this.userModel.findByIdAndUpdate(
        userId,
        {
          $addToSet: { following: targetUserId },
          $inc: { followingCount: 1 },
        },
        { session },
      );
 
      // Tambahkan ke followers
      await this.userModel.findByIdAndUpdate(
        targetUserId,
        {
          $addToSet: { followers: userId },
          $inc: { followersCount: 1 },
        },
        { session },
      );
 
      await session.commitTransaction();
    } catch (error) {
      await session.abortTransaction();
      throw error;
    } finally {
      session.endSession();
    }
  }
 
  async unfollowUser(userId: string, targetUserId: string) {
    const session = await this.userModel.startSession();
    session.startTransaction();
 
    try {
      await this.userModel.findByIdAndUpdate(
        userId,
        {
          $pull: { following: targetUserId },
          $inc: { followingCount: -1 },
        },
        { session },
      );
 
      await this.userModel.findByIdAndUpdate(
        targetUserId,
        {
          $pull: { followers: userId },
          $inc: { followersCount: -1 },
        },
        { session },
      );
 
      await session.commitTransaction();
    } catch (error) {
      await session.abortTransaction();
      throw error;
    } finally {
      session.endSession();
    }
  }
 
  async getFollowers(userId: string, limit: number = 20) {
    return this.userModel
      .findById(userId)
      .populate({
        path: 'followers',
        select: 'username avatar bio',
        options: { limit },
      });
  }
 
  async getFollowing(userId: string, limit: number = 20) {
    return this.userModel
      .findById(userId)
      .populate({
        path: 'following',
        select: 'username avatar bio',
        options: { limit },
      });
  }
}

Langkah 4: Posts Service

src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Post } from '../schemas/post.schema';
import { Comment } from '../schemas/comment.schema';
import { User } from '../schemas/user.schema';
 
@Injectable()
export class PostsService {
  constructor(
    @InjectModel(Post.name) private postModel: Model<Post>,
    @InjectModel(Comment.name) private commentModel: Model<Comment>,
    @InjectModel(User.name) private userModel: Model<User>,
  ) {}
 
  async createPost(userId: string, content: string, images: string[] = []) {
    const post = new this.postModel({
      userId,
      content,
      images,
    });
 
    const savedPost = await post.save();
 
    // Increment user's post count
    await this.userModel.findByIdAndUpdate(userId, {
      $inc: { postsCount: 1 },
    });
 
    return savedPost;
  }
 
  async getPostById(postId: string) {
    return this.postModel
      .findById(postId)
      .populate('userId', 'username avatar')
      .populate({
        path: 'comments',
        populate: { path: 'userId', select: 'username avatar' },
      });
  }
 
  async getUserFeed(userId: string, page: number = 1, limit: number = 10) {
    const skip = (page - 1) * limit;
 
    // Dapatkan user's following list
    const user = await this.userModel.findById(userId);
    const followingIds = user.following;
 
    // Dapatkan posts dari following users
    return this.postModel
      .find({
        userId: { $in: followingIds },
        visibility: { $in: ['public', 'friends'] },
      })
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .populate('userId', 'username avatar')
      .populate({
        path: 'comments',
        options: { limit: 3 },
        populate: { path: 'userId', select: 'username avatar' },
      });
  }
 
  async likePost(postId: string, userId: string) {
    return this.postModel.findByIdAndUpdate(
      postId,
      {
        $addToSet: { likes: userId },
        $inc: { likesCount: 1 },
      },
      { new: true },
    );
  }
 
  async unlikePost(postId: string, userId: string) {
    return this.postModel.findByIdAndUpdate(
      postId,
      {
        $pull: { likes: userId },
        $inc: { likesCount: -1 },
      },
      { new: true },
    );
  }
 
  async addComment(postId: string, userId: string, content: string) {
    const comment = new this.commentModel({
      postId,
      userId,
      content,
    });
 
    const savedComment = await comment.save();
 
    // Tambahkan comment ke post
    await this.postModel.findByIdAndUpdate(postId, {
      $push: { comments: savedComment._id },
      $inc: { commentsCount: 1 },
    });
 
    return savedComment;
  }
 
  async getTrendingPosts(limit: number = 10) {
    return this.postModel.aggregate([
      {
        $match: { visibility: 'public' },
      },
      {
        $addFields: {
          score: {
            $add: [
              { $multiply: ['$likesCount', 2] },
              '$commentsCount',
              { $multiply: ['$sharesCount', 3] },
            ],
          },
        },
      },
      {
        $sort: { score: -1, createdAt: -1 },
      },
      {
        $limit: limit,
      },
      {
        $lookup: {
          from: 'users',
          localField: 'userId',
          foreignField: '_id',
          as: 'user',
        },
      },
      {
        $unwind: '$user',
      },
      {
        $project: {
          content: 1,
          images: 1,
          likesCount: 1,
          commentsCount: 1,
          sharesCount: 1,
          score: 1,
          createdAt: 1,
          'user.username': 1,
          'user.avatar': 1,
        },
      },
    ]);
  }
 
  async getPostAnalytics(postId: string) {
    return this.postModel.aggregate([
      {
        $match: { _id: new Types.ObjectId(postId) },
      },
      {
        $lookup: {
          from: 'comments',
          localField: '_id',
          foreignField: 'postId',
          as: 'allComments',
        },
      },
      {
        $project: {
          content: 1,
          likesCount: 1,
          commentsCount: 1,
          sharesCount: 1,
          createdAt: 1,
          totalEngagement: {
            $add: ['$likesCount', '$commentsCount', '$sharesCount'],
          },
          engagementRate: {
            $divide: [
              { $add: ['$likesCount', '$commentsCount', '$sharesCount'] },
              { $max: [1, { $size: '$allComments' }] },
            ],
          },
        },
      },
    ]);
  }
}

Langkah 5-10: Controllers, Module, Docker & Testing

[Sama seperti versi English - struktur identik dengan terjemahan komentar]

Testing Sistem

Test endpoints
# Buat user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "username": "alice",
    "password": "password123"
  }'
 
# Dapatkan user profile
curl http://localhost:3000/users/USER_ID
 
# Follow user
curl -X PUT http://localhost:3000/users/TARGET_USER_ID/follow \
  -H "Content-Type: application/json" \
  -d '{"userId": "USER_ID"}'
 
# Buat post
curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "USER_ID",
    "content": "Hello, world!",
    "images": []
  }'
 
# Dapatkan user feed
curl "http://localhost:3000/posts/feed/USER_ID?page=1&limit=10"
 
# Dapatkan trending posts
curl "http://localhost:3000/posts/trending?limit=10"
 
# Like post
curl -X PUT http://localhost:3000/posts/POST_ID/like \
  -H "Content-Type: application/json" \
  -d '{"userId": "USER_ID"}'
 
# Tambahkan comment
curl -X POST http://localhost:3000/posts/POST_ID/comments \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "USER_ID",
    "content": "Great post!"
  }'
 
# Dapatkan post analytics
curl http://localhost:3000/posts/POST_ID/analytics
 
# Dapatkan followers
curl http://localhost:3000/users/USER_ID/followers
 
# Dapatkan following
curl http://localhost:3000/users/USER_ID/following

Kesalahan Umum & Pitfalls

1. Tidak Menggunakan Indexes

Queries tanpa indexes lambat di large collections.

ts
// ❌ Salah - tidak ada index
db.users.find({ email: "user@example.com" })
 
// ✅ Benar - buat index
db.users.createIndex({ email: 1 })
db.users.find({ email: "user@example.com" })

2. Menyimpan Large Arrays

Arrays yang grow unbounded cause performance issues.

ts
// ❌ Salah - array grow indefinitely
{
  _id: 1,
  comments: [comment1, comment2, ..., comment1000000]
}
 
// ✅ Benar - gunakan separate collection
// posts collection
{ _id: 1, commentsCount: 1000000 }
 
// comments collection
{ postId: 1, content: "..." }

3. Tidak Menggunakan Transactions

Multi-step operations tanpa transactions bisa leave data inconsistent.

ts
// ❌ Salah - tidak ada transaction
await users.updateOne({ _id: 1 }, { $inc: { balance: -100 } })
await users.updateOne({ _id: 2 }, { $inc: { balance: 100 } })
// Jika second fail, data inconsistent
 
// ✅ Benar - gunakan transaction
const session = await db.startSession()
session.startTransaction()
try {
  await users.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session })
  await users.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session })
  await session.commitTransaction()
} catch (error) {
  await session.abortTransaction()
}

4. Inefficient Aggregations

Complex aggregations tanpa proper stages lambat.

ts
// ❌ Salah - inefficient
db.posts.aggregate([
  { $lookup: { from: "users", ... } },
  { $lookup: { from: "comments", ... } },
  { $match: { status: "published" } }  // Filter setelah joins
])
 
// ✅ Benar - filter early
db.posts.aggregate([
  { $match: { status: "published" } },  // Filter first
  { $lookup: { from: "users", ... } },
  { $lookup: { from: "comments", ... } }
])

5. Tidak Handle Connection Errors

Connection failures harus handled gracefully.

ts
// ✅ Proper error handling
try {
  const user = await this.userModel.findById(userId)
  return user
} catch (error) {
  if (error.name === 'MongoNetworkError') {
    throw new ServiceUnavailableException('Database unavailable')
  }
  throw error
}

6. Tidak Validate Data

Invalid data bisa corrupt database Anda.

ts
// ✅ Gunakan schema validation
UserSchema.pre('save', async function (next) {
  if (!this.email.includes('@')) {
    throw new Error('Invalid email')
  }
  next()
})

Best Practices

1. Design Schemas Thoughtfully

Balance antara normalization dan denormalization.

ts
// ✅ Good schema design
// Users collection
{ _id: 1, name: "Alice", postsCount: 5 }
 
// Posts collection
{ _id: 1, userId: 1, content: "...", likesCount: 10 }
 
// Comments collection
{ _id: 1, postId: 1, userId: 2, content: "..." }

2. Gunakan Appropriate Indexes

Index fields yang digunakan di queries dan sorts.

ts
// ✅ Buat indexes untuk common queries
db.posts.createIndex({ userId: 1, createdAt: -1 })
db.posts.createIndex({ status: 1 })
db.comments.createIndex({ postId: 1, createdAt: -1 })

3. Implementasikan Pagination

Avoid loading semua documents sekaligus.

ts
// ✅ Paginate results
const page = 1
const limit = 20
const skip = (page - 1) * limit
 
const posts = await this.postModel
  .find()
  .skip(skip)
  .limit(limit)
  .sort({ createdAt: -1 })

4. Gunakan Aggregation Pipeline

Leverage aggregation untuk complex queries.

ts
// ✅ Gunakan aggregation untuk analytics
const stats = await this.postModel.aggregate([
  { $match: { status: "published" } },
  { $group: { _id: "$userId", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
  { $limit: 10 }
])

5. Monitor Performance

Track slow queries dan optimize.

Monitor Performance
# Enable profiling
db.setProfilingLevel(1, { slowms: 100 })
 
# View slow queries
db.system.profile.find({ millis: { $gt: 100 } }).pretty()

6. Implementasikan Proper Error Handling

Handle MongoDB-specific errors.

ts
// ✅ Handle MongoDB errors
try {
  await this.userModel.create(userData)
} catch (error) {
  if (error.code === 11000) {
    throw new ConflictException('Email already exists')
  }
  throw error
}

7. Gunakan Connection Pooling

Reuse connections untuk better performance.

ts
// ✅ Connection pooling configured di MongooseModule
MongooseModule.forRoot(mongoUri, {
  maxPoolSize: 10,
  minPoolSize: 5,
})

MongoDB vs SQL vs Other NoSQL

FeatureMongoDBPostgreSQLCassandra
SchemaFlexibleRigidFlexible
TransactionsMulti-docACIDLimited
JoinsAggregationNativeNo
ScalabilityHorizontalVerticalHorizontal
ConsistencyEventualStrongEventual
Query LanguageMQLSQLCQL

Pilih MongoDB ketika:

  • Perlu flexible schemas
  • Rapid development required
  • Horizontal scaling needed
  • Document-oriented data

Pilih PostgreSQL ketika:

  • Complex relationships
  • ACID transactions critical
  • Strong consistency needed
  • Structured data

Pilih Cassandra ketika:

  • Massive scale required
  • High availability critical
  • Time-series data
  • Write-heavy workloads

Kesimpulan

MongoDB adalah powerful document database yang enable rapid development dan horizontal scaling. Memahami core concepts—documents, collections, indexing, dan aggregations—mengaktifkan Anda untuk build scalable applications.

Contoh social media platform mendemonstrasikan production patterns:

  • Flexible document schemas
  • Efficient indexing strategies
  • Complex aggregation pipelines
  • Transaction handling
  • Relationship management
  • Real-time analytics

Key takeaways:

  1. Gunakan MongoDB untuk flexible, document-oriented data
  2. Design schemas thoughtfully
  3. Buat appropriate indexes
  4. Gunakan aggregation pipelines untuk complex queries
  5. Implementasikan transactions untuk consistency
  6. Monitor performance dan optimize
  7. Handle errors gracefully

Mulai dengan simple collections dan queries. Seiring complexity tumbuh, explore advanced patterns seperti sharding, replica sets, dan change streams. Fleksibilitas MongoDB membuatnya suitable untuk systems dari simple CRUD applications hingga complex analytics platforms.

Langkah selanjutnya:

  1. Setup MongoDB locally dengan Docker
  2. Bangun simple CRUD application
  3. Tambahkan indexes dan optimize queries
  4. Implementasikan aggregation pipelines
  5. Monitor dan optimize performance

MongoDB mentransformasi bagaimana Anda think tentang data modeling—dari rigid schemas ke flexible documents. Master it, dan Anda akan bangun applications yang scale dengan business Anda.


Related Posts