So far, we have satisfied the access patterns around creation and retrieval of core entities in our mobile application, such as Users and Photos. We have also learned how to use an inverted index to enable additional query patterns on our entities.

In this module, we will satisfy two access patterns:

  • React to a photo (Write)
  • Follow friend (Write)

Note that both of these access patterns are writing data to DynamoDB, in contrast to the read-heavy patterns we’ve done so far

To satisfy the both access patterns in the steps below, we’re going to use DynamoDB ACID transactions. DynamoDB transactions were released in November 2018. Let’s get a quick primer on how transactions in DynamoDB work.

Transactions are popular in relational systems for operations that affect multiple data elements at once. For example, imagine you are running a bank. One customer, Karen, transfers $100 to another customer, Sujith. When recording this transaction, you would use a transaction to make sure the changes are applied to the balances of both customers rather than just one.

The addition of transactions to DynamoDB makes it easier to build applications where you need to alter multiple items as part of a single operation. With DynamoDB transactions, you can operate on up to 10 items as part of a transaction request.

In a TransactWriteItem API call, you can use the following types of operations.

  • Put: For inserting or overwriting an item;
  • Update: For updating an existing item;
  • Delete: For removing an item;
  • ConditionCheck: For asserting a condition on an existing item without altering the item.

In the steps below, we will use a DynamoDB transactions in two ways to handle complex operations.

Time to Complete Module: 20 Minutes


  • Step 1: React to a photo

    The first access pattern we will address in this module is reacting to a photo.

    When adding a user’s reaction to a photo, we need to do a few things:

    • Confirm that the user has not already used this reaction type on this photo
    • Create a new Reaction entity to store the reaction
    • Increment the proper reaction type in the reactions property on the Photo entity so that we can display the reaction details on a photo

    Note that this requires write actions across two different items -- the existing Photo entity and the new Reaction entity -- as well as conditional logic for one of the items. This is the kind of operation that is a perfect fit for DynamoDB transactions.

    In the code you downloaded, there is a script in the application/ directory called add_reaction.py that includes a function for adding a reaction to a photo. The function in that file uses a DynamoDB transaction to add a reaction.

    The contents of the file are as follows:

    import datetime
    
    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    REACTING_USER = 'kennedyheather'
    REACTION_TYPE = 'sunglasses'
    PHOTO_USER = 'ppierce'
    PHOTO_TIMESTAMP = '2019-04-14T08:09:34'
    
    
    def add_reaction_to_photo(reacting_user, reaction_type, photo_user, photo_timestamp):
        reaction = "REACTION#{}#{}".format(reacting_user, reaction_type)
        photo = "PHOTO#{}#{}".format(photo_user, photo_timestamp)
        user = "USER#{}".format(photo_user)
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "quick-photos",
                            "Item": {
                                "PK": {"S": reaction},
                                "SK": {"S": photo},
                                "reactingUser": {"S": reacting_user},
                                "reactionType": {"S": reaction_type},
                                "photo": {"S": photo},
                                "timestamp": {"S": datetime.datetime.now().isoformat() }
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        },
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": user}, "SK": {"S": photo}},
                            "UpdateExpression": "SET reactions.#t = reactions.#t + :i",
                            "ExpressionAttributeNames": {
                                "#t": reaction_type
                            },
                            "ExpressionAttributeValues": {
                                ":i": { "N": "1" },
                            },
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"
                        }
                    }
                ]
            )
            print("Added {} reaction from {}".format(reaction_type, reacting_user))
            return True
        except Exception as e:
            print("Could not add reaction to photo")
    
    add_reaction_to_photo(REACTING_USER, REACTION_TYPE, PHOTO_USER, PHOTO_TIMESTAMP)
    

    In the add_reaction_to_photo function, we’re using the transact_write_items() method to perform a write transaction. Our transaction has two operations.

    First, we’re doing a Put operation to insert a new Reaction entity. As part of that operation, we’re specifying a condition that the SK attribute should not exist for this item. This is a way to ensure that an item with this PK and SK doesn’t already exist. If it did, that would mean the user has already added this reaction to this photo.

    The second operation is an Update operation on the User entity to increment the reaction type in the reactions attribute map. DynamoDB’s powerful update expressions allow you to perform atomic increments without needing to first retrieve the item and then update it.

    Run this script with the following command in your terminal.

    python application/add_reaction.py

    The output in your terminal should indicate that the reaction was added to the photo.

    Added sunglasses reaction from kennedyheather

    Note that if you try to run the script again, the function will fail. The user kennedyheather has already added this reaction to this photo, so trying to do it again would violate the condition expression in the operation to create the Reaction entity. In other words, the function is idempotent and repeated invocations of it with the same inputs will not have unintended consequences.

    The addition of DynamoDB transactions greatly simplifies the workflow around complex operations like these. Previously, this would have required multiple API calls with complex conditions and manual rollbacks in the event of conflicts. Now it can be implemented with less than 50 lines of code.

    In the next step, we’ll see how to handle our “Follow user” access pattern.

  • Step 2: Following a user

    In your application, one user can follow another user. When the application backend gets a request to follow a user, we need to do four things:

    • Check that the following user is not already following the requested user;
    • Create a Friendship entity to record the following relationship;
    • Increment the follower count for the user being followed;
    • Increment the following count for the user following.

    In the code you downloaded, there is a file in the application/ directory called follow_user.py. The contents of the file are as follows:

    import datetime
    
    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    FOLLOWED_USER = 'tmartinez'
    FOLLOWING_USER = 'john42'
    
    
    def follow_user(followed_user, following_user):
        user = "USER#{}".format(followed_user)
        friend = "#FRIEND#{}".format(following_user)
        user_metadata = "#METADATA#{}".format(followed_user)
        friend_user = "USER#{}".format(following_user)
        friend_metadata = "#METADATA#{}".format(following_user)
        try:
            resp = dynamodb.transact_write_items(
                TransactItems=[
                    {
                        "Put": {
                            "TableName": "quick-photos",
                            "Item": {
                                "PK": {"S": user},
                                "SK": {"S": friend},
                                "followedUser": {"S": followed_user},
                                "followingUser": {"S": following_user},
                                "timestamp": {"S": datetime.datetime.now().isoformat()},
                            },
                            "ConditionExpression": "attribute_not_exists(SK)",
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": user}, "SK": {"S": user_metadata}},
                            "UpdateExpression": "SET followers = followers + :i",
                            "ExpressionAttributeValues": {":i": {"N": "1"}},
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                    {
                        "Update": {
                            "TableName": "quick-photos",
                            "Key": {"PK": {"S": friend_user}, "SK": {"S": friend_metadata}},
                            "UpdateExpression": "SET following = following + :i",
                            "ExpressionAttributeValues": {":i": {"N": "1"}},
                            "ReturnValuesOnConditionCheckFailure": "ALL_OLD",
                        }
                    },
                ]
            )
            print("User {} is now following user {}".format(following_user, followed_user))
            return True
        except Exception as e:
            print(e)
            print("Could not add follow relationship")
    
    follow_user(FOLLOWED_USER, FOLLOWING_USER)

    The follow_user function in the file is similar to a function you would have in your application. It takes two usernames -- one of the followed user and one of the following user -- and it runs a request to create a Friendship entity and update the two User entities.

    Run the script in your terminal with the following command.

    python application/follow_user.py

    You should see output in your terminal indicating that the operation succeeded.

    User john42 is now following user tmartinez

    Try running the script a second time in your terminal. This time, you should get an error message indicating that you could not add the follow relationship. This is because this user now follows the requested user, so our request then failed the conditional check on the item.

  • Conclusion

    In this module, we saw how to satisfy two advanced write operations in our application. First, we used DynamoDB transactions for having a user react to a photo. With transactions, we handled a complex conditional write across multiple items in a single request. Further, we saw how to use DynamoDB update expressions to increment a nested attribute in a map property.

    Second, we implemented the function for a user to follow another user. This required altering three items in a single request, while also performing a conditional check on one of the items. While this would normally be a difficult operation, DynamoDB makes it simple to handle this with DynamoDB transactions.

    In the next module, we’ll clean up the resources we created and see some next steps in our DynamoDB learning path.