Introducing Dynago

We are using Go at Under Armour to build some of our high-performance microservices. Its focus on performance and concurrency is compelling to us, so we’ve been looking to grow our Go toolchain to match what we have for some other languages.

We recently had a use case that was well-suited for DynamoDB but we found that building complex queries in existing Go packages was difficult. To simplify our implementation and allow us to write more readable, maintainable code, we chose to create a new DynamoDB library and contribute it back to the community.

To illustrate the complexity of building real-world queries consider the following sample document:

{
    "id":   123,
    "name": "Bob",
    "age":  45,
    "address": {
        "city": "Boston",
        ...
    },
    "tags": ["male", "middle_aged"],
}

This schema illustrates a range of value types with simple nesting—common things you might want to persist in a document store. Let’s say Bob just got a job and you want to insert a new “job” key, and also add a tag with the value “employed” and only do so if “job” is not set.

Here is what that looks like as a conditional update in AWS-SDK-Go:

svc.UpdateItem(&dynamodb.UpdateItemInput{
    TableName: aws.String("People"),
    Key: map[string]*dynamodb.AttributeValue{
        "id": {N: aws.String(strconv.FormatInt(123, 10))},
    },
    ConditionExpression: aws.String("attribute_not_exists(#j)"),
    UpdateExpression:    aws.String("SET #j=:job ADD tags :t1"),
    ExpressionAttributeNames: map[string]*string{
        "#j": aws.String("job"),
    },
    ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
        ":job":  {S: aws.String("Engineer")},
        ":t1":   {S: aws.String("employed")},
    },
    ReturnValues: aws.String(dynamodb.ReturnValueNone),
})

So what’s with all the extra magic bits like the S, N, M, SS, and so on? Well those are details of DynamoDB’s underlying protocol, for which you must specify the type of each value that follows, further followed by a specific encoding of the value for the type. This leaves you dealing with underlying protocol concerns instead of just your data. For example, numbers are keyed by N: followed by a string representation of the number. But why should the library user care about these details?

In building a simple library for DynamoDB, we strove to make real use cases more simple to express. Also, we logically grouped functionality (like condition expressions and parameter values) to aid the developer. Here’s the same operation expressed with Dynago:

client.UpdateItem("People", dynago.HashKey("id", 123)).
    ConditionExpression("attribute_not_exists(#j)", dynago.P("#j", "job")).
    UpdateExpression("SET #j=:job ADD tags :t1").
    Params(dynago.Document{":job": "Engineer", ":t1": "employed"}).
    ReturnValues(dynago.ReturnUpdatedNew).
    Execute()

Queries in Dynago are built using chaining (reminiscent of ORMs in other languages) resulting in a query that can then be executed. Partially built queries can be stored and reused or extended with conditions, attributes, and query parameters as needed. Chaining is also friendly to tab-completion, making it easy to discover the operations that are available.

Dynago allows you to use Go’s built-in types like string, int, float64, and bool for interacting with DynamoDB data, except in cases where it would be ambiguous. For example, string sets are disambiguated from lists by the StringSet type.

Dynago is on available on GitHub and published documentation is available on GoDoc.

We love the Go community and the strong set of libraries that are available and want to do our part to contribute. We welcome your feedback and contributions.

dynamodbgolang