Getting Started
Prerequisites
In DynamoDB, tables, items, and attributes are the core components that you work with. A table is a collection of items, and each item is a collection of attributes. DynamoDB uses primary keys to uniquely identify each item in a table and secondary indexes to provide more querying flexibility.
To learn more about DynamoDB, check out the official developer guide.
Get Tempest¶
First, add Tempest to your project.
For AWS SDK 1.x:
dependencies {
implementation "app.cash.tempest:tempest:1.10.0"
}
For AWS SDK 2.x:
dependencies {
implementation "app.cash.tempest:tempest2:1.10.0"
}
Start Coding¶
Let’s build a URL shortener with the following features:
- Creating custom aliases from a short URL to a destination URL.
- Redirecting existing short URLs to destination URLs.
We express it like this in code.
interface UrlShortener {
/**
* Creates a custom alias from [shortUrl] to [destinationUrl].
* @return false if [shortUrl] is taken.
*/
fun shorten(shortUrl: String, destinationUrl: String): Boolean
/**
* Redirects [shortUrl] to its destination.
* @return null if not found.
*/
fun redirect(shortUrl: String): String?
}
public interface UrlShortener {
/**
* Creates a custom alias from {@code shortUrl} to {@code destinationUrl}.
* @return false if {@code shortUrl} is taken.
*/
boolean shorten(String shortUrl, String destinationUrl);
/**
* Redirects {@code shortUrl} to its destination.
* @return null if not found.
*/
@Nullable
String redirect(String shortUrl);
}
We will store URL aliases in the following table.
Primary Key | Attributes |
short_url | |
SquareCLA | destination_url |
https://docs.google.com/forms/d/e/1FAIpQLSeRVQ35-gq2vdSxD1kdh7CJwRdjmUA0EZ9gRXaWYoUeKPZEQQ/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 | |
KindleWireless | destination_url |
http://www.amazon.com/Kindle-Wireless-Reading-Display-Globally/dp/B003FSUDM4/ref=amb_link_353259562_2?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-10&pf_rd_r=11EYKTN682A79T370AM3&pf_rd_t=201&pf_rd_p=1270985982&pf_rd_i=B002Y27P3M | |
BestUrlShortener | destination_url |
https://www.google.com/search?q=best+url+shortener&oq=best+url+shortener&aqs=chrome..69i57j69i64l2.8705j0j1&sourceid=chrome&ie=UTF-8 | |
... |
To access this table in code, model it using
DynamoDBMapper
or
DynamoDbEnhancedClient
.
Note: The base item type
AliasItem
is still used for theLogicalTable
. This type is intended to model an empty row, so all its fields should be nullable with anull
default value. Using non-nullable types or fields with default values will cause issues during serialization and querying.
// Note: this POJO is not type-safe because its attributes are nullable and mutable.
@DynamoDbBean
class AliasItem {
@get:DynamoDbPartitionKey
var short_url: String? = null
var destination_url: String? = null
}
// Note: this POJO is not type-safe because its attributes are nullable and mutable.
@DynamoDbBean
public class AliasItem {
private String short_url;
private String destination_url;
@DynamoDbPartitionKey
@DynamoDbAttribute("short_url")
public String getShortUrl() {
return short_url;
}
public void setShortUrl(String short_url) {
this.short_url = short_url;
}
@DynamoDbAttribute("destination_url")
public String getDestinationUrl() {
return destination_url;
}
public void setDestinationUrl(String destination_url) {
this.destination_url = destination_url;
}
}
// Note: this POJO is not type-safe because its attributes are nullable and mutable.
@DynamoDBTable(tableName = "alias_items")
class AliasItem {
@DynamoDBHashKey
var short_url: String? = null
@DynamoDBAttribute
var destination_url: String? = null
}
// Note: this POJO is not type-safe because its attributes are nullable and mutable.
@DynamoDBTable(tableName = "alias_items")
public class AliasItem {
private String shortUrl;
private String destinationUrl;
@DynamoDBHashKey(attributeName = "short_url")
public String getShortUrl() {
return shortUrl;
}
public void setShortUrl(String short_url) {
this.shortUrl = short_url;
}
@DynamoDBAttribute(attributeName = "destination_url")
public String getDestinationUrl() {
return destinationUrl;
}
public void setDestinationUrl(String destination_url) {
this.destinationUrl = destination_url;
}
}
Tempest lets you interact with AliasItem
using strongly typed data classes.
interface AliasDb : LogicalDb {
@TableName("alias_items")
val aliasTable: AliasTable
}
interface AliasTable : LogicalTable<AliasItem> {
val aliases: InlineView<Alias.Key, Alias>
}
data class Alias(
val short_url: String,
val destination_url: String
) {
data class Key(
val short_url: String
)
}
public interface AliasDb extends LogicalDb {
@TableName("alias_items")
AliasTable aliasTable();
}
public interface AliasTable extends LogicalTable<AliasItem> {
InlineView<Alias.Key, Alias> aliases();
}
public class Alias {
public final String short_url;
public final String destination_url;
public Alias(String short_url, String destination_url) {
this.short_url = short_url;
this.destination_url = destination_url;
}
public Key key() {
return new Key(short_url);
}
public static class Key {
public final String short_url;
public Key(String short_url) {
this.short_url = short_url;
}
}
}
interface AliasDb: LogicalDb {
val aliasTable: AliasTable
}
interface AliasTable : LogicalTable<AliasItem> {
val aliases: InlineView<Alias.Key, Alias>
}
data class Alias(
val short_url: String,
val destination_url: String
) {
data class Key(
val short_url: String
)
}
public interface AliasDb extends LogicalDb {
AliasTable aliasTable();
}
public interface AliasTable extends LogicalTable<AliasItem> {
InlineView<Alias.Key, Alias> aliases();
}
public class Alias {
public final String short_url;
public final String destination_url;
public Alias(String short_url, String destination_url) {
this.short_url = short_url;
this.destination_url = destination_url;
}
public Key key() {
return new Key(short_url);
}
public static class Key {
public final String short_url;
public Key(String short_url) {
this.short_url = short_url;
}
}
}
Let’s put everything together.
class RealUrlShortener(
private val table: AliasTable
) : UrlShortener {
override fun shorten(shortUrl: String, destinationUrl: String): Boolean {
val item = Alias(shortUrl, destinationUrl)
val ifNotExist = Expression.builder()
.expression("attribute_not_exists(short_url)")
.build()
return try {
table.aliases.save(item, ifNotExist)
true
} catch (e: ConditionalCheckFailedException) {
println("Failed to shorten $shortUrl because it already exists!")
false
}
}
override fun redirect(shortUrl: String): String? {
val key = Alias.Key(shortUrl)
return table.aliases.load(key)?.destination_url
}
}
fun main(args: Array<String>) {
val client = DynamoDbEnhancedClient.create()
val db = LogicalDb<AliasDb>(client)
urlShortener = RealUrlShortener(db.aliasTable)
urlShortener.shorten("tempest", "https://cashapp.github.io/tempest")
}
public class RealUrlShortener implements UrlShortener {
private final AliasTable table;
public RealUrlShortener(AliasTable table) {
this.table = table;
}
@Override
public boolean shorten(String shortUrl, String destinationUrl) {
Alias item = new Alias(shortUrl, destinationUrl);
Expression ifNotExist = Expression.builder()
.expression("attribute_not_exists(short_url)")
.build();
try {
table.aliases().save(item, ifNotExist);
return true;
} catch (ConditionalCheckFailedException e) {
System.out.println("Failed to shorten $shortUrl because it already exists!");
return false;
}
}
@Override
@Nullable
public String redirect(String shortUrl) {
Alias.Key key = new Alias.Key(shortUrl);
Alias alias = table.aliases().load(key);
if (alias == null) {
return null;
}
return alias.destination_url;
}
}
public static void main(String[] args) {
DynamoDbEnhancedClient client = DynamoDbEnhancedClient.create();
AliasDb db = LogicalDb.create(AliasDb.class, client);
UrlShortener urlShortener = new RealUrlShortener(db.aliasTable());
urlShortener.shorten("tempest", "https://cashapp.github.io/tempest");
}
class RealUrlShortener(
private val table: AliasTable
) : UrlShortener {
override fun shorten(shortUrl: String, destinationUrl: String): Boolean {
val item = Alias(shortUrl, destinationUrl)
val ifNotExist = DynamoDBSaveExpression()
.withExpectedEntry("short_url", ExpectedAttributeValue()
.withExists(false))
return try {
table.aliases.save(item, ifNotExist)
true
} catch (e: ConditionalCheckFailedException) {
println("Failed to shorten $shortUrl because it already exists!")
false
}
}
override fun redirect(shortUrl: String): String? {
val key = Alias.Key(shortUrl)
return table.aliases.load(key)?.destination_url
}
}
fun main(args: Array<String>) {
val client: AmazonDynamoDB = AmazonDynamoDBClientBuilder.standard().build()
val mapper: DynamoDBMapper = DynamoDBMapper(client)
val db: AliasDb = LogicalDb(mapper)
val urlShortener = RealUrlShortener(db.aliasTable)
urlShortener.shorten("tempest", "https://cashapp.github.io/tempest")
}
public class RealUrlShortener implements UrlShortener {
private final AliasTable table;
public RealUrlShortener(AliasTable table) {
this.table = table;
}
@Override
public boolean shorten(String shortUrl, String destinationUrl) {
Alias item = new Alias(shortUrl, destinationUrl);
DynamoDBSaveExpression ifNotExist = new DynamoDBSaveExpression()
.withExpectedEntry(
"short_url",
new ExpectedAttributeValue().withExists(false));
try {
table.aliases().save(item, ifNotExist);
return true;
} catch (ConditionalCheckFailedException e) {
System.out.println("Failed to shorten $shortUrl because it already exists!");
return false;
}
}
@Override
@Nullable
public String redirect(String shortUrl) {
Alias.Key key = new Alias.Key(shortUrl);
Alias alias = table.aliases().load(key);
if (alias == null) {
return null;
}
return alias.destination_url;
}
}
public static void main(String[] args) {
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard().build();
DynamoDBMapper mapper = new DynamoDBMapper(client);
AliasDb db = LogicalDb.create(AliasDb.class, mapper);
UrlShortener urlShortener = new RealUrlShortener(db.aliasTable());
urlShortener.shorten("tempest", "https://cashapp.github.io/tempest");
}
Check out the code samples on Github: