- Add detailed CouchDB design document with denormalization strategy - Create migration script for MongoDB to CouchDB transition - Implement CouchDB service layer with all CRUD operations - Add query examples showing performance improvements - Design supports embedded data for better read performance - Include Mango indexing strategy for optimal query patterns - Provide data consistency and migration strategies This design prioritizes read performance and user experience for the social community nature of the Adopt-a-Street application. 🤖 Generated with AI Assistant Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
11 KiB
CouchDB Query Examples
This document demonstrates how the CouchDB design handles common query patterns for the Adopt-a-Street application.
1. User Authentication
MongoDB Query
const user = await User.findOne({ email: "john@example.com" });
CouchDB Equivalent
// Using Mango query
const user = await couchdbService.findUserByEmail("john@example.com");
// Raw Mango query
{
"selector": {
"type": "user",
"email": "john@example.com"
},
"limit": 1
}
Performance: Single document lookup with indexed email field.
2. Geospatial Street Search
MongoDB Query
const streets = await Street.find({
location: {
$geoWithin: {
$box: [[-74.1, 40.6], [-73.9, 40.8]]
}
},
status: "available"
});
CouchDB Equivalent
// Using service method
const streets = await couchdbService.findStreetsByLocation([[-74.1, 40.6], [-73.9, 40.8]]);
// Raw Mango query
{
"selector": {
"type": "street",
"status": "available",
"location": {
"$geoWithin": {
"$box": [[-74.1, 40.6], [-73.9, 40.8]]
}
}
}
}
Performance: Geospatial index on location field, filtered by status.
3. User's Activity Feed
MongoDB Query (Multiple Queries)
const posts = await Post.find({ user: userId }).sort({ createdAt: -1 });
const tasks = await Task.find({ completedBy: userId }).sort({ updatedAt: -1 });
const events = await Event.find({ participants: userId }).sort({ date: -1 });
// Combine and sort results
const activity = [...posts, ...tasks, ...events].sort((a, b) => b.createdAt - a.createdAt);
CouchDB Equivalent (Single Query)
// Using service method
const activity = await couchdbService.getUserActivity(userId, 50);
// Raw Mango query
{
"selector": {
"$or": [
{
"type": "post",
"user.userId": "user_1234567890abcdef"
},
{
"type": "task",
"completedBy.userId": "user_1234567890abcdef"
},
{
"type": "event",
"participants": {
"$elemMatch": {
"userId": "user_1234567890abcdef"
}
}
}
]
},
"sort": [{"createdAt": "desc"}],
"limit": 50
}
Performance: Single query with compound OR selector, sorted by creation date.
4. Social Feed with User Data
MongoDB Query (Population Required)
const posts = await Post.find({})
.populate('user', 'name profilePicture')
.sort({ createdAt: -1 })
.limit(20);
// For comments, separate query needed
const comments = await Comment.find({ post: { $in: posts.map(p => p._id) } })
.populate('user', 'name profilePicture')
.sort({ createdAt: 1 });
CouchDB Equivalent (No Population Needed)
// Get posts with embedded user data
const posts = await couchdbService.getSocialFeed(20);
// Get comments for posts
const postIds = posts.map(p => p._id);
const comments = await couchdbService.getPostComments(postIds[0]); // Example for one post
// Raw Mango query for posts
{
"selector": {
"type": "post"
},
"sort": [{"createdAt": "desc"}],
"limit": 20
}
// Raw Mango query for comments
{
"selector": {
"type": "comment",
"post.postId": {"$in": ["post_123", "post_456"]}
},
"sort": [{"createdAt": "asc"}]
}
Performance: User data embedded in posts, no additional lookups needed.
5. Leaderboard
MongoDB Query
const users = await User.find({ points: { $gt: 0 } })
.select('name points profilePicture')
.sort({ points: -1 })
.limit(10);
CouchDB Equivalent
// Using service method
const leaderboard = await couchdbService.getLeaderboard(10);
// Raw Mango query
{
"selector": {
"type": "user",
"points": {"$gt": 0}
},
"sort": [{"points": "desc"}],
"limit": 10,
"fields": ["_id", "name", "points", "profilePicture", "stats"]
}
Performance: Indexed query on points field with descending sort.
6. Street Details with Related Data
MongoDB Query (Multiple Queries)
const street = await Street.findById(streetId).populate('adoptedBy', 'name profilePicture');
const tasks = await Task.find({ street: streetId });
const reports = await Report.find({ street: streetId });
// Calculate stats manually
const stats = {
tasksCount: tasks.length,
completedTasksCount: tasks.filter(t => t.status === 'completed').length,
reportsCount: reports.length,
openReportsCount: reports.filter(r => r.status === 'open').length
};
CouchDB Equivalent (Single Document)
// Single document contains all needed data
const street = await couchdbService.getById(streetId);
// Raw Mango query
{
"selector": {
"_id": "street_abc123def456"
}
}
// Result includes embedded stats:
{
"_id": "street_abc123def456",
"type": "street",
"name": "Main Street",
"adoptedBy": {
"userId": "user_123",
"name": "John Doe",
"profilePicture": "https://cloudinary.com/image.jpg"
},
"stats": {
"tasksCount": 5,
"completedTasksCount": 3,
"reportsCount": 2,
"openReportsCount": 1
}
}
Performance: Single document lookup with pre-calculated stats.
7. Event Management
MongoDB Query
const events = await Event.find({
date: { $gte: new Date() },
status: "upcoming"
}).populate('participants', 'name profilePicture');
CouchDB Equivalent
// Using service method
const events = await couchdbService.findByType('event', {
date: { $gte: new Date().toISOString() },
status: 'upcoming'
});
// Raw Mango query
{
"selector": {
"type": "event",
"date": {"$gte": "2024-01-15T00:00:00Z"},
"status": "upcoming"
},
"sort": [{"date": "asc"}]
}
// Result includes embedded participant data:
{
"_id": "event_123",
"type": "event",
"title": "Community Cleanup",
"participants": [
{
"userId": "user_123",
"name": "John Doe",
"profilePicture": "https://cloudinary.com/image.jpg",
"joinedAt": "2024-01-10T10:00:00Z"
}
],
"participantsCount": 1
}
Performance: Participant data embedded, no population needed.
8. Badge System
MongoDB Query (Complex Join)
const user = await User.findById(userId).populate({
path: 'earnedBadges',
populate: {
path: 'badge',
model: 'Badge'
}
});
CouchDB Equivalent (Embedded Data)
// Single user document contains badge data
const user = await couchdbService.findUserById(userId);
// Raw Mango query
{
"selector": {
"_id": "user_1234567890abcdef"
}
}
// Result includes embedded badges:
{
"_id": "user_1234567890abcdef",
"type": "user",
"name": "John Doe",
"earnedBadges": [
{
"badgeId": "badge_123",
"name": "Street Hero",
"description": "Adopted 5 streets",
"icon": "🏆",
"rarity": "rare",
"earnedAt": "2024-01-15T10:30:00Z",
"progress": 100
}
],
"stats": {
"badgesEarned": 1
}
}
Performance: Badge data embedded in user document, no joins required.
9. Point Transaction History
MongoDB Query
const transactions = await PointTransaction.find({ user: userId })
.sort({ createdAt: -1 })
.limit(50);
CouchDB Equivalent
// Using service method
const transactions = await couchdbService.find({
type: 'point_transaction',
'user.userId': userId
}, {
sort: [{ createdAt: 'desc' }],
limit: 50
});
// Raw Mango query
{
"selector": {
"type": "point_transaction",
"user.userId": "user_1234567890abcdef"
},
"sort": [{"createdAt": "desc"}],
"limit": 50
}
// Result includes embedded user data:
{
"_id": "transaction_123",
"type": "point_transaction",
"user": {
"userId": "user_123",
"name": "John Doe"
},
"amount": 10,
"type": "task_completion",
"description": "Completed task: Clean up litter",
"relatedEntity": {
"entityType": "Task",
"entityId": "task_456",
"entityName": "Clean up litter"
},
"balanceAfter": 150
}
Performance: Indexed query on user and creation date.
10. Real-time Updates with Changes Feed
MongoDB (Change Streams)
const changeStream = User.watch();
changeStream.on('change', (change) => {
// Handle user changes
});
CouchDB (Changes Feed)
// Listen to changes feed
const changes = couchdbService.db.changes({
since: 'now',
live: true,
include_docs: true
});
changes.on('change', (change) => {
const doc = change.doc;
// Handle different document types
switch (doc.type) {
case 'user':
// Handle user updates
break;
case 'post':
// Handle new posts
break;
case 'event':
// Handle event updates
break;
}
});
// Filter by document type
const userChanges = couchdbService.db.changes({
since: 'now',
live: true,
include_docs: true,
filter: '_design/app',
selector: {
type: 'user'
}
});
Performance: Native real-time updates with filtering capabilities.
Performance Comparison Summary
| Query Pattern | MongoDB | CouchDB | Performance Impact |
|---|---|---|---|
| User Auth | 1 query + index | 1 query + index | Similar |
| Social Feed | 1 query + populate | 1 query (embedded) | CouchDB faster |
| User Activity | 3 queries + combine | 1 query (OR) | CouchDB faster |
| Leaderboard | 1 query + index | 1 query + index | Similar |
| Street Details | 4 queries + calc | 1 query (embedded) | CouchDB much faster |
| Event Management | 1 query + populate | 1 query (embedded) | CouchDB faster |
| Badge System | Complex populate | Embedded data | CouchDB much faster |
| Real-time Updates | Change Streams | Changes Feed | CouchDB more flexible |
Key Benefits of CouchDB Design
- Reduced Query Complexity: Most common queries become single-document lookups
- Better Read Performance: Embedded data eliminates JOIN operations
- Simplified Application Logic: No need for complex population strategies
- Improved Offline Support: Rich documents enable better offline functionality
- Real-time Capabilities: Native changes feed with flexible filtering
- Scalability: Denormalized design scales well with read-heavy workloads
Trade-offs and Mitigations
-
Data Duplication: User data appears in multiple documents
- Mitigation: Use changes feed to propagate updates
-
Update Complexity: Changes require updating multiple documents
- Mitigation: Batch updates and background reconciliation jobs
-
Storage Overhead: Larger documents due to embedded data
- Mitigation: Selective embedding based on access patterns
-
Consistency: Eventual consistency for related data
- Mitigation: Application-level consistency checks and reconciliation
This CouchDB design prioritizes read performance and user experience, which aligns perfectly with the social community nature of the Adopt-a-Street application where most operations are reads rather than writes.