AWS Database Blog

Making coordinated changes to multiple items with Amazon DynamoDB transactions

The use of NoSQL databases has increased significantly in recent years as more and more organizations see NoSQL databases as solutions that free them from the constraints of a relational database management system (RDBMS). While the flexibility, agility, and performance of NoSQL databases are the main benefits triggering the shift towards them, the popularity of RDBMS remained the same because of some crucial requirements of organizations.

RDBMSs are often favored over NoSQL databases for critical data operations because of transactional support, their most widely known and discussed feature.

Why use Amazon DynamoDB transactions?

A number of applications always need business logic that requires an ‘atomic’ or all-or-nothing database operation on one or more items. This provides you with the ability to roll back all of the database operations in case of any one faulty operation. Trying to address this need usually makes the application implementation difficult in terms of tracking all connected operations and reverse them accordingly.

Transactions was announced at re:Invent 2018 as a DynamoDB new feature. It provides native support for transactions within the database to provide ACID (atomicity, consistency, isolation, durability) on multiple items within a single AWS account and Region.

Traditionally, DynamoDB supported these properties for a single item only. It was designed for ensuring availability and durability of data. Transactions added atomicity (all-or-nothing) and isolation (transactions-not-affecting-each-other) for one or more tables on multiple items.

Additionally, the transactions ClientRequestToken key enables the API call to be idempotent, so that multiple identical calls don’t replay the same operation and have the same effect as one single call.

With transactions, you no longer have to worry about or struggle with rollback operations within the database. Transactions helps you maintain data integrity by coordinating actions across multiple items and tables.

How it works

DynamoDB transactions currently provides two main API actions, called TransactWriteItems and TransactGetItems. You can combine several actions and submit them as a single all-or-nothing operation.

For example: you’ve decided to move your hotel’s reservation management application from a relational database to DynamoDB. The following diagram represents your ER structure. In the diagram, there are 3 entities: Guest, Reservation and Room. There is a many-to-many relationship between Guest and Reservation and also many-to-many relationship between Reservation and Room.

You apply a heterogeneous tables approach here and map three legacy tables into one DynamoDB table. Hotel reservation data model on DynamoDB with sample data is shown in the following table.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “501” }
OccupiesRooms : { “20014” }
Reservation “501” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “501”

Example code

Guests can perform three kinds of operations: create reservation, check in, and check out. This post covers these three scenarios, wrapped into three different Java methods, to show DynamoDB’s transactional capabilities, as shown in the following diagram. In the diagram user named can perform three operations sequentially, the first is Create reservation, second is Check-in and third is Check-out.

First of all, you must create a table to work on.

private static void createTable(String tableName) {
        try {
        	// Create a table with a primary hash key named 'Id', which holds a string
            CreateTableRequest createTableRequest = new CreateTableRequest().withTableName(tableName)
                .withKeySchema(new KeySchemaElement().withAttributeName("Id").withKeyType(KeyType.HASH))
                .withAttributeDefinitions(new AttributeDefinition().withAttributeName("Id").withAttributeType(ScalarAttributeType.S))
                .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L));

            // Create table if it does not exist yet
            TableUtils.createTableIfNotExists(dynamoDB, createTableRequest);

            // wait for the table to move into ACTIVE state
        	TableUtils.waitUntilActive(dynamoDB, tableName);
        	
            // Describe our new table
            DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(tableName);
            TableDescription tableDescription = dynamoDB.describeTable(describeTableRequest).getTable();
            System.out.println("Table Created Successfully. Table Description: " + tableDescription);
        	
		} catch (InterruptedException e) {
        	System.out.println("Occupied thread is interrupted");
		}
    }

Create reservation

Next, create a set of records to represent a sample reservation in the system.

    private static void createReservation() {
    	
        // Create guest item
    	HashMap<String, AttributeValue> guestItem = new HashMap<String, AttributeValue>();
    	guestItem.put("Id", new AttributeValue("John"));
    	guestItem.put("ActiveReservations", new AttributeValue("500"));
    	
        // Create room item
    	HashMap<String, AttributeValue> roomItem = new HashMap<String, AttributeValue>();
    	roomItem.put("Id", new AttributeValue("R20014"));
    	roomItem.put("RoomStatus", new AttributeValue("FREE"));
        
        // Create reservation item
    	HashMap<String, AttributeValue> reservationItem = new HashMap<String, AttributeValue>();
    	reservationItem.put("Id", new AttributeValue("500"));
    	reservationItem.put("GuestId", new AttributeValue("John"));
    	reservationItem.put("ReservationStatus", new AttributeValue("PENDING"));

        Put createGuest = new Put().withTableName(TABLE_NAME).withItem(guestItem);
        Put createRoom = new Put().withTableName(TABLE_NAME).withItem(roomItem);
        Put createReservation = new Put().withTableName(TABLE_NAME).withItem(reservationItem);

        Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withPut(createGuest),
                new TransactWriteItem().withPut(createRoom),
                new TransactWriteItem().withPut(createReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

	    // Execute the transaction and process the result.
		// The following code snippet illustrates how to execute the actions defined previously as a single all-or-nothing operation
        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Create Reservation transaction is successful");
    }

The table after the create reservation operation is shown below.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “500”}
OccupiesRooms : null
Reservation “500” GuestId: “John”
ReservationStatus: “PENDING”
FulfilledByRoom: null
Room “R20014” RoomStatus: “FREE”
RentedToReservation : null

Check in

The next operation is check in. During this transaction, all three items(Guest, Reservation, and Room) in the table are updated.

    private static void checkIn() {
    	
    	//Create updateGuest object
    	HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
    	guestItemKey.put("Id", new AttributeValue("John"));
    	
    	HashMap<String, AttributeValue> guestExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	guestExpressionAttributeValues.put(":occupies_rooms", new AttributeValue("R20014"));
    	
    	Update updateGuest = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(guestItemKey)
    			.withUpdateExpression("SET OccupiesRooms = :occupies_rooms")
    			.withExpressionAttributeValues(guestExpressionAttributeValues);

    	
    	//Create updateRoom object
    	HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
    	roomItemKey.put("Id", new AttributeValue("R20014"));
    	
    	HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	roomExpressionAttributeValues.put(":room_status", new AttributeValue("OCCUPIED"));
    	roomExpressionAttributeValues.put(":rented_to_reservation", new AttributeValue("500"));
    	
    	Update updateRoom = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(roomItemKey)
    			.withUpdateExpression("SET RoomStatus = :room_status, RentedToReservation = :rented_to_reservation")
    			.withExpressionAttributeValues(roomExpressionAttributeValues);

    	//Create updateReservation object
    	HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
    	reservationItemKey.put("Id", new AttributeValue("500"));
    	
    	HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("FULLFILLED"));
    	reservationExpressionAttributeValues.put(":fullfilled_by_room", new AttributeValue("R20014"));
    	
    	Update updateReservation = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(reservationItemKey)
    			.withUpdateExpression("SET ReservationStatus = :reservation_status, FullfilledByRoom = :fullfilled_by_room")
    			.withExpressionAttributeValues(reservationExpressionAttributeValues);
    	
    	//Execute transaction
    	Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withUpdate(updateGuest),
                new TransactWriteItem().withUpdate(updateRoom),
                new TransactWriteItem().withUpdate(updateReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Check-in transaction is successful");
    	
    }

The table after the check in operation is shown below.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “500”}
OccupiesRooms : { “R20014” }
Reservation “500” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “500”

Check out

The last operation is check out. Since null or empty attributes are not allowed in DynamoDB, some attributes are removed.

    private static void checkOut() {
    	//Create updateGuest object
    	HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
    	guestItemKey.put("Id", new AttributeValue("John"));
    	
    	Update updateGuest = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(guestItemKey)
    			.withUpdateExpression("REMOVE OccupiesRooms, ActiveReservations");
    	//Since there is no value in the attribute, it is removed
    	
    	//Create updateRoom object
    	HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
    	roomItemKey.put("Id", new AttributeValue("R20014"));
    	
    	HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	roomExpressionAttributeValues.put(":room_status", new AttributeValue("FREE"));
    	
    	Update updateRoom = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(roomItemKey)
    			.withUpdateExpression("SET RoomStatus = :room_status REMOVE RentedToReservation") //Since there is no value in the attribute, it is removed
    			.withExpressionAttributeValues(roomExpressionAttributeValues);
    	
    	//Create updateReservation object
    	HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
    	reservationItemKey.put("Id", new AttributeValue("500"));
    	
    	HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
    	reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("CLOSED"));
    	
    	Update updateReservation = new Update()
    			.withTableName(TABLE_NAME)
    			.withKey(reservationItemKey)
    			.withUpdateExpression("SET ReservationStatus = :reservation_status")
    			.withExpressionAttributeValues(reservationExpressionAttributeValues);
    	
    	//Execute transaction
    	Collection<TransactWriteItem> actions = Arrays.asList(
        		new TransactWriteItem().withUpdate(updateGuest),
                new TransactWriteItem().withUpdate(updateRoom),
                new TransactWriteItem().withUpdate(updateReservation));
        
        TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
                .withTransactItems(actions)
                .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

        dynamoDB.transactWriteItems(createReservationTransaction);
        System.out.println("Check-out transaction is successful");
    }

The table after the check out operation is shown below.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : {}
Reservation “500” GuestId: “John”
ReservationStatus: “CLOSED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “FREE”

Conclusion

DynamoDB transactional APIs simplify the developer experience by providing ACID for any item in any DynamoDB table with no additional cost. Transactions are enabled for all single-region DynamoDB tables by default and can be enabled on global tables optionally. It covers a longstanding need for connected database operations and extends the scale, performance, and enterprise-ready benefits of DynamoDB to a broader set of workloads.

More information on transactions is available in the Amazon DynamoDB Developer Guide.

 


About the Authors

 

Baris Yasin is a Senior Solutions Architect with Amazon Web Services.

 

 

 

 

Serdar Nevruzoglu is a Solutions Architect with Amazon Web Services.