AWS. How you shouldn't write your API.

Guys from AWS go’s team follow good practices. Every bit of code can be easily changed without affecting to other code. Code is verbose and mobile. Every component has interface. Every package has subpackage with defined interfaces. It allow do clean testing. It’s almost a ideal codebase with the exception of that this API incredible complex for developers.
On this week we choose which DB we would use for our new app. 
I had to choose between mongoDB with I worked long time and dynamoDB. I wanted to use DynamoDB until I opened it’s godoc. What is it?

I measured number of items in godoc’s index for some AWS services and compared it with OpenGL.

550 AWS dynamoDB
920 AWS route53
1344 AWS redshift 
1792 AWS s3
2044 AWS rds 
2652 openGL 2

It’s ridiculous!

How to insert two line data to DynamoDB

I got a bit of code from this repository.

params := &dynamodb.BatchWriteItemInput{
RequestItems: map[string][]*dynamodb.WriteRequest{
"Forum": {
&dynamodb.WriteRequest{
PutRequest: &dynamodb.PutRequest{
Item: map[string]*dynamodb.AttributeValue{
"Name": {
S: aws.String("Forum one"),
},
"Views": {
N: aws.String("1"),
},
},
},
},
},
},
}
svc.BatchWriteItem(params)

What is it! Looks like javasript’s callback hell!

function one() {
//...
function two() {
//...
function three() {
//...
function four() {
//...
function oneBillion() {
// callback hell
}
}
}
}
}

Or maybe it’s JAVA. I need create constructor which will be used for creating another constructor which will be used for creating another constructor …

Let’s list steps.

  1. First you need create dynamodb.BatchWriteItemInput
  2. Then set to field RequestItems pointer to map[string][]*dynamodb.WriteRequest
  3. Then create field with name of desired table. Set to this field pointer to dynamodb.WriteRequest
  4. Then set pointer to dynamodb.WriteRequest in this field
  5. Then set to field PutRequest pointer to map[string]*dynamodb.AttributeValue
  6. Then create field for every table’s value and put pointer to dynamodb.AttributeValue
  7. (!!!!)Then choose which type of data you want set. dynamodb.AttributeValue contain fields for string S, for numbers N, for slice of strings NS and so on (!!!!)
  8. Then convert your value to string
  9. Then convert string to aws.String
  10. Profit!

What!

So bad?

When I first time faced with s3 api I was shocked. I spend pretty much time for stupid Hello world.
Then I figured out that there was a s3 manager subpackage which contain only Downloader which contain only Download method and Uploader which contain only Upload method.

Looks like good API. Dave Cheney’s favorite interface is io.Reader which contain only Read(buf []byte) (n int, err error).

type Reader interface {
// Read reads up to len(buf) bytes into buf.
Read(buf []byte) (n int, err error)
}

How it could be

I just copied marshaller from MongoDB’s driver mgo.

package dynamodb
import (
"reflect"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/aws"
"strconv"
)
func (table *Table)Insert(input interface{}) (*dynamodb.BatchWriteItemOutput, error) {
t := reflect.TypeOf(input)
v := reflect.ValueOf(input)
request := map[string]*dynamodb.AttributeValue{}
for i := 0; i < t.NumField(); i++ {
attribute := dynamodb.AttributeValue{}
field := t.Field(i)
switch field.Type.Name() {
case "string":
attribute.S = aws.String(v.FieldByName(field.Name).String())
case "float64":
attribute.S = aws.String(strconv.FormatFloat(v.FieldByName(field.Name).Float(), 'f', 10, 64))
}
request[field.Name] = &attribute
}
params := &dynamodb.BatchWriteItemInput{
RequestItems: map[string][]*dynamodb.WriteRequest{
table.Name: {
&dynamodb.WriteRequest{
PutRequest: &dynamodb.PutRequest{
Item: request,
},
},
},
},
}
return table.DB.BatchWriteItem(params)
}

Here are example how easy it is with new driver.

package main
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
)
const AWS_ZONE = "us-east-1"
const AWS_ID = "AWS_ID"
const AWS_SECRET = "AWS_SECRET"
type Forum struct {
Name string
Category string
Threads float64
Messages float64
Views float64
}
func main() {
sess := session.New(&aws.Config{
Region: aws.String(AWS_ZONE),
Credentials: credentials.NewStaticCredentials(AWS_ID, AWS_SECRET, ""),
})
db := dynamodb.New(sess)
forums = db.Table("Forum")
forum := Forum{
Name: "Name",
Category: "Category",
Threads: 12,
Messages: 13,
Views: 14,
}
result, err := forums.Insert(forum)
if err != nil {
panic(err)
}
}

It’s only one row!!!

result, err := forums.Insert(forum)

Much less frustration. I use reflect package. In community it consider slow and fragile.But mongoDB driver use reflect and even encoding/json. Developer always can use Marchaller interface for speed up. Code got much clear. With struct’s tags can be implemented logic like ‘omitempty’.

Conclusion

AWS api is really “great” but guys forgot that it supposed to be simple not for them but for developers who have to work with it.

Update: thanks to reddit’s user count757
1 If I would be JAVA dev and I would have to work with that API I wouldn’t complain.
2. If I would C++ dev and I would have to work with API which in 10 time more complex I wouldn’t complain.
3. If I am python dev and if some library which have a class which could have __cmp__, __str__ methods and it would be very comfy but it doesn’t have it. I can start to complain.
4. If I am go dev and some library looks like JAVA library and don’t follow rules “keep things simple” and “don’t be smart”. I can start to complain.

I used example of S3 ans S3manager. I don’t know when dev need use S3 package. It’s complicated otherwise S3manager which is adjusted for go. It have only 15 items in godoc’s index. It’s concurrent and can work with big files out of the box. Guys used go’s features and it’s amazing! I love it!

Go has excellent serilization/deserialization mechanism. In reality we work with serilization/deserialization a lot. And go do it simple. DynamoDB could be so easy with go as encoding/json package. People who need more speed always can write own marshaller.

Good API should use language’s paradigms and use language’s features.