Mongodb – not evil, just misunderstood

Lately I’ve been reading a lot about Mongodb and posts dissuading you from ever using it. Some of these articles are seriously outrageous and make me wonder what got the team to actually start using Mongodb in the first place. Sarah Mei’s recent article was one such that upset me a lot, especially since the title was so inflammatory.

My post however, aims at highlighting the areas where Mongodb works and how it performed brilliantly for us. As someone leading the engineering efforts for a shipping and logistics company I wasn’t too happy initially to see Mongodb being used as the primary datastore but after 2 years I’m more than sure that this was definitely the datastore for us. I’ve outlined areas that confused me when i first encountered them only to learn that they were actually invaluable features that were available to me.

“No migrations” is that all you have?
The advantages of schemaless documents are priceless. Not having to migrate is just one of the perks. Our schemas were largely in the form of Orders (having many) Shipments (going_from) ShipPoint (to) ShipPoint

We rarely used most of these entities without the other and it just served us extremely well to manage them as self contained documents embedding the other.

Mongodb writes are fire and forget? WTF?
This doesn’t always have to be the case, though it significantly contributes to Mongodb’s fast writes. Mongodb’s write concerns configurations allow you to configure the precise level of persistence that needs to be achieved to call it a successful write. So if the write fails you know its failed. The fact that you could know if your write has migrated to replicas or has been journaled is a pretty neat feature.

How can the default writes be fire and forget?
(Version – 2.4.8 changes this however this is valid till version 2.2.6)
It just made sense, given all the information to configure it the way you prefer I would always go with this approach. We add a lot of Notes to each shipment as it gets reviewed at different levels by the sales, accounts and other teams. These notes generally serve as a reminder or a single line indicating that its been viewed – though it doesn’t critically affect the business workflows of the application. Its just seemed logical that these were fire and forget operations and could be stored as quickly as possible.

Another place where this is extremely handy for us is during Tracking. We track several hundreds of shipments each day logging every tracking status, location and time the shipment has been, while in transit. This information is handy for customers to keep an eye on where their shipment has reached. Chances are when fetching this information some of the information is not saved the first time – but we expect that it would be obtained during a second fetch 30 minutes later. The default write concern works brilliantly then.

Read locks and write locks – don’t they slow you down
They do but since most of the stuff is memory mapped this doesn’t affect you in a major way. However, I did notice people always working the primary of a replicaSet and never querying the secondaries for fear of inconsistent data. I think if you have sufficient memory your replication lag would be pretty small and besides if you don’t need the data to be consistent every instant querying secondary is a sensible option to reduce the load off your primary. Which brings me to the PrimaryPreferred Read preference. This allows you to query a secondary in your replica set when your primary is not available. It’s a fairly safe choice in my opinion.

We began querying secondaries for ShipPoints which didn’t change that often.

All the memory usage is killing me!
This is one of the things I that took me time to accept. Mongodb expects that your working set fits into RAM along with the indexes for your database. Your working set is the data that is frequently queried and updated. Since mongodb works with memory maps most of your working set data is mapped to the memory. When this data is not available in memory a page fault occurs and your data needs to be fetched from disk. This results in a performance penalty but as long as you have some swap space you can safely load the data back in.

While our working set was fairly small our reporting application needed access to the entire shipment records to generate reports. This resulted in Mongo running out of memory and spitting OperationFailure errors on a regular basis.

Our initial approach was naive and we started using Redis(which is another datastore thats pure gold.) to store snapshots of information but soon realised we could just use Mongodb to make it work.

So can I never generate reports without having my dataset fit in memory?
Rollups to the rescue. Rollups are pre-aggregated statistical information that help you speed up your aggregation process. This makes life significantly easier as you query for short time ranges to generate micro-reports.

Here is a simplified snapshot on how we generated daily and monthly aggregates with mapreduce.

class AggregatesGenerator
def self.generate_daily_aggregates(date = nil)
map = <<-MAP
var key = {date: new Date(this.record_created_at.getFullYear(),
var data = {}; =;
data['zone_' +] = 1;
data.service_type_string = this.service_type.toString().toLowerCase();
data[this.service_type.toString().toLowerCase()] = 1;
data.total_price_cents = parseInt(this.price_cents);
data.shipment = this.tracking_id
if(this.carrier == 'FEDEX'){
data.FEDEX = 1;
data.UPS = 0;
data.UPS = 1;
data.FEDEX = 0;
emit(key, data);
reduce = <<-REDUCE
function(key, values){
var results = {'next_day_air': 0,
'second_day': 0,
'three_day_select': 0,
'ground': 0,
'fedex_2_day': 0,
'zone_2': 0,
'zone_3': 0,
'zone_4': 0,
'zone_5': 0,
'zone_6': 0,
'zone_7': 0,
'zone_8': 0,
'zone_9': 0,
'zone_10': 0,
'zone_14': 0,
'zone_17': 0,
'zone_22': 0,
'zone_23': 0,
'zone_25': 0,
'zone_92': 0,
'zone_96': 0,
'total_price_cents': 0,
'UPS': 0,
results.total_price_cents += parseInt(v.total_price_cents);
results[v.service_type_string] += v[v.service_type_string];
results['zone_' +] += v['zone_' +];
var today = new Date()
results.updated_at = new Date(today.getFullYear(),
results.FEDEX += v.FEDEX;
results.UPS += v.UPS;
return results;
finalize = <<-FINALIZE
function(key, results){
var fields = [
results[f] = 0;
results.shipments = [];
return results;
if date.nil?
ReportingShipment.order_by([:record_created_at, :desc]).
map_reduce(map, reduce).out(merge: 'daily_aggregates').finalize(finalize).find
ReportingShipment.where({:record_created_at.gte => date.beginning_of_day, => date.end_of_day}).
map_reduce(map,reduce).out(merge: 'daily_aggregates').find
def self.generate_monthly_aggregates
map = <<-MAP
var objDate =;
var key = {
year: objDate.getFullYear(),
month: objDate.getMonth()
emit(key, this.value);
reduce = <<-REDUCE
function(key, values){
var result = {
result.next_day_air += v.next_day_air;
result.second_day += v.second_day;
result.three_day_select += v.three_day_select;
result.ground += v.ground;
result.fedex_ground += v.fedex_ground;
result.total_price_cents += parseInt(v.total_price_cents);
result.zone_2 += v.zone_2;
result.zone_3 += parseInt(v.zone_3);
result.zone_4 += parseInt(v.zone_4);
result.zone_5 += parseInt(v.zone_5);
result.zone_6 += parseInt(v.zone_6);
result.zone_7 += parseInt(v.zone_7);
result.zone_8 += parseInt(v.zone_8);
result.zone_9 += parseInt(v.zone_9);
result.zone_10 += parseInt(v.zone_10);
result.zone_14 += parseInt(v.zone_14);
result.zone_17 += parseInt(v.zone_17);
result.zone_22 += parseInt(v.zone_22);
result.zone_23 += parseInt(v.zone_23);
result.zone_25 += parseInt(v.zone_25);
result.zone_92 += parseInt(v.zone_92);
result.zone_96 += parseInt(v.zone_96);
return result;
DailyAggregate.order_by([:_id, :desc]).map_reduce(map, reduce).
out(merge: 'monthly_aggregates')

view raw


hosted with ❤ by GitHub

So you mean this can’t be realtime?
Yes it can – through atomic updates. Just like we generated rollups to speed up reporting we can generate pre-aggregated snapshots of aggregated information like this.

{ "_id" :
{ "year" : 2013, "month" : 3 },
"value" :
{ "next_day_air" : 30,
"second_day" : 83,
"three_day_select" : 7,
"ground" : 27,
"fedex_ground" : 51,
"priority_overnight" : 0,
"fedex_express_saver" : 0,
"fedex_2_day" : 0,
"total_price_cents" : 0,
"zone_2" : 10,
"zone_3" : 10,
"zone_4" : 12,
"zone_5" : 16,
"zone_6" : 158,
"zone_7" : 1,
"zone_8" : 0,
"zone_9" : 0,
"zone_10" : 0,
"zone_14" : 0,
"zone_17" : 0,
"zone_22" : 0,
"zone_23" : 0,
"zone_25" : 0,
"zone_92" : 0,
"zone_96" : 0,

Once this is in place you can update your aggregates by simply incrementing the right counter with something like"value.next_day_air", 1)

I haven’t even touched upon the replication and sharding features that Mongodb offers which I will reserve for another post. To summarise I feel Mongodb is awesome and is a lot like the kid in class who you dismissed because your friends thought he was weird – till you got to know him.

Disclaimer: I don’t claim to be an authority on Mongodb and everything that I have written about is stuff that I’ve learnt while working with Mongodb. I recommend reading the documentation and going through the talks available on the Mongodb website.

6 thoughts on “Mongodb – not evil, just misunderstood

  1. Have you looked at TokuMX from Tokutek? It gives you support for transactions and a nice performance boost to boot. Sounds like it might be a good fit for what you are doing.

      • As every new software, tokumx has also its own quirks. However, we got around them and are able to use it in production. We subscribed to the enterprise edition to have technical support from tokutec.
        The benefits: using mongodb, we had 3 shards, each one consisting of 3 replicaset members, each member having 4.5 TB of SSD’s in RAID 10, makes a total of 13.5 TB of the whole cluster.
        Using tokumx, the same quantity of data fits in only one replicaset of only 3 TB. So, instead of 9 servers we need only 3 of them and have still more capacity.
        Also, a (partial) restore which took 16 hours with mongodb takes only 2 or 3 hours with tokumx in normal insert mode (there exists also a bulk mode which took only 1 hour but it’s still a bit buggy in v1.3.2 – probably fixed in v1.3.3).
        Well, as I said, there are still some issues with tokumx (mongodb isn’t bugfree either) but it’s quite promising nevertheless!

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s