CouchDB and Domain-Driven Design

I’ve found CouchDB to be a great fit for domain-driven design (DDD). Specifically, CouchDB fits very well with the building block patterns and practices found within DDD. Two of these building blocks include Entities and Value Objects. Entities are objects defined by a thread of continuity and identity. A Value Object “is an object that describes some characteristic or attribute but carries no concept of identity.” Value objects should be treated as immutable.

Aggregates are groupings of associated Entities and Value Objects. Within an Aggregate, one member is designated as the Aggregate Root. External references are limited to only the Aggregate Root. Aggregates should follow transaction, distribution, and concurrency boundaries. Guess what else is defined by transaction, distribution, and concurrency boundaries? That’s right, JSON documents in CouchDB.

Let’s take a look at an example Aggregate, that representing a blog entry and related metadata. Note that the following UML diagrams are for classes in PHP, but it should be easy enough to translate these examples to any object-oriented programming language. We’ll start with the Entry Entity, which will serve as our Aggregate Root:

-----------------------------------------
|                 Entry                 |
-----------------------------------------
|+ id : string                          |
|+ rev : string                         |
|+ title : Text                         |
|+ updated : Date                       |
|+ authors : Person[*]                  |
|+ content : Text                       |
-----------------------------------------
|+ __construct(entry : array) : void    |
|+ toArray() : array                    |
-----------------------------------------

The Text Value Object:

----------------------------------------------
|                    Text                    |
----------------------------------------------
|- type : string                             |
|- text : string                             |
----------------------------------------------
|+ __construct(type : string, text : string) |
|+ toArray() : array                         |
----------------------------------------------

The Date Value Object:

--------------------------------------
|                Date                |
--------------------------------------
|- timestamp : integer               |
--------------------------------------
|+ __construct(timestamp : integer)  |
|+ __toString() : string             |
--------------------------------------

The Person Value Object:

-------------------------------------------------------------
|                           Person                          |
-------------------------------------------------------------
|- name : string                                            |
|- uri : string                                             |
|- email : string                                           |
-------------------------------------------------------------
|+ __construct(name : string, uri : string, email : string) |
|+ toArray() : array                                        |
-------------------------------------------------------------

I recommend serializing each Aggregate, starting with the Aggregate Root, into a JSON document. Control access to Aggregate Roots through a Repository. The toArray() methods above return an associative array representation of each object. The Repository can then transform the array into JSON for storage in CouchDB. Let’s take a look at the EntryRepository:

---------------------------------
|        EntryRepository        |
---------------------------------
|                               |
---------------------------------
|+ get(id : string) : Entry     |
|+ post(entry : Entry) : void   |
|+ put(entry : Entry) : void    |
|+ delete(entry : Entry) : void |
---------------------------------

Here’s an example of what the Aggregate’s object graph might look like, serialized as a JSON document:

{
    "_id": "http://bradley-holt.com/?p=1251",
    "title": {
        "type": "text",
        "text": "CouchDB and Domain-Driven Design"
    },
    "updated": "2011-08-02T15:30:00+00:00",
    "authors": [
        {
             "name": "Bradley Holt",
             "uri": "http://bradley-holt.com/",
             "email": "bradley.holt@foundline.com"
        }
    ],
    "content": {
        "type": "html",
        "text": "<p>I've found CouchDB to be a great fit for…</p>"
    }
}

You can also provide access to CouchDB views through Repositories. In the above example, this could be through the addition of an index(skip : integer, limit : integer) : Entry[*] method to the the EntryRepository (note that this is a naive pagination implementation, especially on large data sets—but that’s beyond the scope of this blog post). For more complex views, you may want to create a separate Repository for each CouchDB view.

5 Comments

  1. Posted August 2, 2011 at 3:01 pm | Permalink

    not sure i follow, perhaps it is too high level, do you have an itro for couchdb programming? can you lower your explanations for a newbie to get up to speed in understanding this please? thanks!

  2. Posted August 2, 2011 at 3:44 pm | Permalink

    I think, that a Person is not a ValueObject because it has a thread of continuity and identity (specificaly in the domain of a blog entry). Also I think that in context of persisting entities and other objects you may use Doctrine 2 Object Document Mapper for MongoDb.

  3. Posted August 3, 2011 at 9:03 am | Permalink

    @cordoval: Check out the slidecast from a presentation I did at NY PHP about CouchDB:
    http://www.slideshare.net/bradley.holt/couchdb-at-new-york-php

  4. Posted August 3, 2011 at 9:25 am | Permalink

    @sokzzuka: There are a couple of reasons why I modeled a Person as a Value Object instead of an Entity. First, it made the explanation of the main topic much easier. If the Aggregate contains objects that are Entities (other than the Aggregate Root), it would have brought up several other design questions that could have distracted from the main point.

    Second, I was basing my model on the Atom Entry Document in the Atom Syndication Format, referencing this as a Published Language[1]. Following that model, I interpreted the Person construct[2] as a Value Object. A Person “has one required element, name, and two optional elements: uri, email.” An Entity must have an identity. Without extending or modifying the model, which element would you pick as the identifier? URI or email seem to make sense, but those are optional fields. Identifiers can’t be optional. The name field is not an appropriate identifier as more than one Person can have the same name (and thus different URIs and emails). Since there is no appropriate identifier, that leaves us with treating a Person as a Value Object within the Atom Syndication Format domain.

    [1]: Domain-Driven Design, Part IV. Strategic Design, Chapter 14. Maintaining Model Integrity, Published Language
    [2]: http://www.atomenabled.org/developers/syndication/#person

  5. Posted September 5, 2011 at 11:52 am | Permalink

    We already implemented a CouchDB persistence backend (http://git.typo3.org/FLOW3/Packages/CouchDB.git) for FLOW3, a DDD centric PHP framework. It works exactly like stated in your article, but we don’t persist complete aggregates for now, since it is somewhat more pragmatic being able to access each entity individually. Right now value objects are embedded into the document which fits the paradigms nicely. Maybe the document aggregate boundary mapping will come later.