AWS – DynamoDB

Dynamodb is a low latency NoSQL database like MongoDB or Cassandra. It is fully managed by AWS so you don’t have to maintain any server for your database. Literally, there are no servers to provision, patch, or manage and no software to install, maintain or operate. DynamoDB automatically scales tables up and down to adjust for capacity and maintain performance. It supports both document and key-value data models. It uses SSD storage so it is very fast and supports Multi-AZ. DynamoDB global tables replicate your data across multiple AWS Regions to give you fast, local access to data for your globally distributed applications. For use cases that require even faster access with microsecond latency, DynamoDB Accelerator (DAX) provides a fully managed in-memory cache.

As with other AWS services, DynamoDB requires a role or user to have the right privileges to access DynamoDB. Make sure you add DynamoDB full access to the role or user(access keys) your server is using. You can also use a special IAM condition to restrict users to only access their data.

DynamoDB supports ACID transactions to enable you to build business-critical applications at scale. DynamoDB encrypts all data by default and provides fine-grained identity and access control on all your tables.

DynamoDB provides both provisioned and on-demand capacity modes so that you can optimize costs by specifying capacity per workload or paying for only the resources you consume.

When to use:
a. Gaming
b. User, vehicle, and driver data stores
c. Ads technology
d. Player session history data stores
e. Inventory tracking and fulfillment
f. Shopping carts
g. User transactions

There are two ways Dynamodb supports read:
1. Eventually consistent read – When data is saved to Dynamodb, it takes about 1 second for the data to propagate across multiple availability zones. This does not guarantee that your users will see the recent data from 1 second ago.
2. Strongly consistent read – This returns the most recent or up to date data. It will detect if there is a write operation happening at the time. If there is, then it will wait to read data afterward which results in a longer but more consistent read.

DynamoDB Tables

  • Tables
  • Items (rows in SQL DB)
  • Attributes (columns in SQL DB)
  • Data can be in JSON, HTML, or XML

DynamoDB Primary Keys
1. Unique Partition Key – This key must be unique across items or rows i.e user_id. This value is input into a hash function which calculates the physical location of which data will be stored.

2. Composite Key (Partition key + Sort key) – This is useful for post or comment tables where rows belong to another entity i.e user_id as the partition key and post_timestamp for the sort key. Two or more items can have the same partition key but they must have different sort keys. These items stored physically together but sorted with their sort key.

DynamoDB Indexes

Local Secondary Index
– Can only be created when creating your table and you cannot change, remove, or add it later.
– Has the same partition as your original table.
– Has a different sort key from your original table.
– Queries based on the local secondary indexes are faster than regular queries.

Global Secondary Index
– Can add when creating your table or later.
– Has different partition and sort keys.
– Speeds up queries.

DynamoDB with Java. I am using DynamoDB wrapper.

For local development, I am using a docker image. There are other ways to install DynamoDB locally.

Run DynamoDB locally on your computer

@Bean
	public AmazonDynamoDB amazonDynamoDB() {
		AmazonDynamoDB client =  AmazonDynamoDBClientBuilder.standard()
				.withCredentials(amazonAWSCredentialsProvider())
				.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(amazonDynamoDBEndpoint, Regions.US_WEST_2.getName()))
				.build();
		return client;
	}
	
	@Bean
	public DynamoDBMapper dynamoDBMapper() {
		return new DynamoDBMapper(amazonDynamoDB());
	}
@DynamoDBTable(tableName="user")
public class User implements Serializable {

	private static final long serialVersionUID = 1L;
	
	@DynamoDBHashKey(attributeName="uuid")  
	@DynamoDBAttribute
	private String uuid;

	@DynamoDBAttribute
	private String email;
	
	@DynamoDBAttribute
	private String firstName;
	
	@DynamoDBAttribute
	private String lastName;
	
	@DynamoDBAttribute
	private String phoneNumber;
	
	@DynamoDBAttribute
	private Date createdAt;
        
        // setters and getters
}
@Repository
public class UserRepositoryImp implements UserRepository {
	
	private Logger log = LoggerFactory.getLogger(this.getClass());

	@Autowired
	private AmazonDynamoDB amazonDynamoDB;
	
	@Autowired
	private DynamoDBMapper dynamoDBMapper;
	
	@Override
	public User create(User user) {
		user.setUuid(RandomGeneratorUtils.getUserUuid());
		user.setCreatedAt(new Date());
		dynamoDBMapper.save(user);
		return getById(user.getUuid());
	}

	@Override
	public User getById(String id) {
		User user = dynamoDBMapper.load(User.class, id);
		return user;
	}



	@Override
	public List<User> getAllUser() {
		PaginatedScanList<User> users = dynamoDBMapper.scan(User.class, new DynamoDBScanExpression());
		
		return (users!=null) ? users.subList(0, users.size()) : null;
	}
	
	@Override
	public boolean createTable() {
		
		
		// check if table has been created
		try {
			DescribeTableResult describeTableResult = amazonDynamoDB.describeTable("user");
			
			if(describeTableResult.getTable()!=null){
				log.debug("user table has been created already!");
				return true;
			}
		} catch (Exception e) {
			
		}
		
		// table hasn't been created so start a createTableRequest
		CreateTableRequest createTableRequest = dynamoDBMapper.generateCreateTableRequest(User.class);
		createTableRequest.withProvisionedThroughput(new ProvisionedThroughput(5L,5L));
		
		// create table
		CreateTableResult createTableResult = amazonDynamoDB.createTable(createTableRequest);
		
		long count = createTableResult.getTableDescription().getItemCount();
		
		log.debug("item count={}",count);
		
		return false;
	}

}

Transaction

Here is an example of sending money(balance) from one user to another.

	@Override
	public boolean tranferBalance(double amount, User userA, User userB) {
		final String USER_TABLE_NAME = "user";
		final String USER_PARTITION_KEY = "userid";
		
		try {
			// user A
			HashMap<String, AttributeValue> userAKey = new HashMap<>();
			userAKey.put(USER_PARTITION_KEY, new AttributeValue(userA.getUuid()));
			
			ConditionCheck checkUserAValid = new ConditionCheck()
			        .withTableName(USER_TABLE_NAME)
			        .withKey(userAKey)
			        .withConditionExpression("attribute_exists(" + USER_PARTITION_KEY + ")");
			
			Map<String, AttributeValue> expressionAttributeValuesA = new HashMap<>();
			expressionAttributeValuesA.put(":balance", new AttributeValue().withN("" + (userA.getBalance() - amount)));

			Update withdrawFromA = new Update().withTableName(USER_TABLE_NAME).withKey(userAKey)
					.withUpdateExpression("SET balance = :balance")
					.withExpressionAttributeValues(expressionAttributeValuesA);

		    log.debug("user A setup!");
			
			// user B
			HashMap<String, AttributeValue> userBKey = new HashMap<>();
			userAKey.put(USER_PARTITION_KEY, new AttributeValue(userB.getUuid()));

			ConditionCheck checkUserBValid = new ConditionCheck()
			        .withTableName(USER_TABLE_NAME)
			        .withKey(userBKey)
			        .withConditionExpression("attribute_exists(" + USER_PARTITION_KEY + ")");
			
			Map<String, AttributeValue> expressionAttributeValuesB = new HashMap<>();
			expressionAttributeValuesB.put(":balance", new AttributeValue().withN("" + (userB.getBalance() + amount)));
			
			Update depositToB = new Update().withTableName(USER_TABLE_NAME).withKey(userBKey)
					.withUpdateExpression("SET balance = :balance")
					.withExpressionAttributeValues(expressionAttributeValuesB);
			
			log.debug("user B setup!");
			
			
			HashMap<String, AttributeValue> withdrawItem = new HashMap<>();
			withdrawItem.put(USER_PARTITION_KEY, new AttributeValue(userA.getUuid()));
			withdrawItem.put("balance", new AttributeValue("100"));
			
			// actions
			Collection<TransactWriteItem> actions = Arrays.asList(
					new TransactWriteItem().withConditionCheck(checkUserAValid),
					new TransactWriteItem().withConditionCheck(checkUserBValid),
					new TransactWriteItem().withUpdate(withdrawFromA),
					new TransactWriteItem().withUpdate(depositToB));
			
			log.debug("actions setup!");

			// transaction request
			TransactWriteItemsRequest withdrawTransaction = new TransactWriteItemsRequest()
					.withTransactItems(actions)
					.withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

			log.debug("transaction request setup!");
			
			// Execute the transaction and process the result.

			TransactWriteItemsResult transactWriteItemsResult = amazonDynamoDB.transactWriteItems(withdrawTransaction);
			log.debug("consumed capacity={}",ObjectUtils.toJson(transactWriteItemsResult.getConsumedCapacity()));
			
			return (transactWriteItemsResult.getConsumedCapacity()!=null) ? true : false;
		} catch (ResourceNotFoundException e) {
			log.error("One of the table involved in the transaction is not found " + e.getMessage());
		} catch (InternalServerErrorException e) {
			log.error("Internal Server Error " + e.getMessage());
		} catch (TransactionCanceledException e) {
			log.error("Transaction Canceled " + e.getMessage());
		} catch (Exception e) {
			log.error("Exception, msg={}",e.getLocalizedMessage());
		}

		return false;
	}

Source code on Github




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *