dagblog.
← Archive

MongoDB behind the scenes: how we used it at GoodData


Jiří Tobolka wrote an introduction to the notification API on the GoodData platform. I'd like to walk through the technical details, especially the persistent storage we used for it. In a nutshell, the notification API describes events we care about, such as a project metric changing because sales fell or new orders rose, and the actions we want to trigger when they occur, such as an SMS, email or Twitter message.

Our notification domain model has three basic entities. ChannelConfiguration represents the channel itself, Subscription represents an event and its description, and Trigger determines how to evaluate it, for example on a timer or when data is loaded. The following diagram shows how they relate.

Before we started coding, we faced the usual question of where to put the data. At GoodData we store the vast majority of our data in a good old relational database (RDBMS), with all its limitations. About a year ago we built a proof of concept to see how hard it would be to store this data outside the RDBMS. The characteristics of this data make it a poor fit for decomposing into relations. I'll try to explain the problem using Notifications.

As I mentioned, the domain model consists of three entities. In a relational database the resulting schema would look roughly like this.

As you can see, there are seven tables if we include the ones representing many-to-many relationships. This relational schema introduces several problems. Every new channel, such as email or Twitter, would require a new table. If we wanted to fetch a particular event, represented as a Subscription, together with the channels it should go to, we would need a left outer join. Deletions would also have to cascade because of referential integrity. You could change the schema in various ways, of course, but its complexity would stay roughly the same. Another option would be to store each entity as a document in a BLOB. We would then lose the last advantage an RDBMS offered us, the ability to write queries such as “give me all notifications for a specific project or user.”

The mental leap is to realize that we are working with documents. We read, modify, store and delete them as whole documents. For persistent storage, what we are actually looking for is a document-oriented database. I'm deliberately avoiding the term NoSQL, because the family includes databases that aren't document-oriented. Options such as Riak, Cassandra and Redis are geared toward key/value storage. With a store like that, we'd effectively have to decompose our documents into key/value pairs and gain no advantage over a conventional RDBMS for this use case.

Although we're talking about an object-oriented database, some concepts are much like those in an RDBMS. Documents are organized into collections, the counterparts of tables. A collection can hold documents with different structures or even different types. In a table the columns and their types determine what data you can store. A document consists of attributes, or fields, corresponding to columns, but again without constraints on a particular type, uniqueness and so on. That means there are no restrictions on the structure or organization of the data. This is why we call this kind of database schemaless.

That brings advantages and disadvantages, or more precisely, things you have to take into account. You can easily add new attributes to documents without an offline data migration. If needed, you can migrate documents while the system is running and reduce server downtime. That matters a hell of a lot when you provide a service with an SLA and guaranteed availability. On the other hand, the application logic bears full responsibility for data consistency and checking it.

Back to Notifications and how we store them in a document-oriented database. There are two collections, one for channels and one for the events themselves, or more precisely for their definitions. An event definition contains a trigger specifying how often to check it, the condition to evaluate, the message to send to the configured channels, and metadata about the document. A reference to a particular channel is an attribute holding that channel's identifier. It has no special type, and no constraint requires that the document exists. That check therefore belongs in the application logic.

There are two proven document-oriented databases, MongoDB and CouchDB. The main difference is in how you access the data and write queries. CouchDB uses Map/Reduce functions. The queries themselves are special documents called views that contain the definitions of those functions. In CouchDB you have to define a view to write a query. MongoDB expresses queries with structured JSON objects, which are more declarative than CouchDB's programmatic Map/Reduce. To improve performance, you can define indexes on attributes much as in an RDBMS. MongoDB supports ad hoc queries, which can be built dynamically from user input, for example. It also supports Map/Reduce, but for batch processing of data.

You access CouchDB documents through a REST interface over HTTP, so a regular HTTP client is enough. MongoDB uses a binary wire protocol, which requires a driver for the language you use to access the data.

We weighed the pros and cons and chose MongoDB for several reasons.

  • Querying. Writing a MongoDB query is much easier for mere mortals to understand than writing Map/Reduce functions, and in some ways it resembles SQL. Map/Reduce is still available.
  • Driver. It's pleasant to use on the client side and, most importantly, fast.
  • Scalability. MongoDB can shard data without an obvious hack. That's important to a SaaS company like GoodData.
  • Tools. Backup, restore and monitoring tools, plus an administration console. The tools use JSON and JavaScript.

We deployed MongoDB successfully on both production and development instances and haven't had any serious problems so far. We enable journaling for data consistency within the engine, even though it slows all writes. That hasn't been a problem, since most operations are reads. If the balance changes, we can run MongoDB in a cluster with data replication. Data corruption on one node would then still require intervention from our Ops team, but service availability would be unaffected.

Medium version

Related articles

Related CZ Podcast episodes