{"id":23235052,"url":"https://github.com/cashapp/tempest","last_synced_at":"2025-04-04T07:04:05.000Z","repository":{"id":37076023,"uuid":"270829467","full_name":"cashapp/tempest","owner":"cashapp","description":"Typesafe DynamoDB for Kotlin and Java.","archived":false,"fork":false,"pushed_at":"2025-03-17T13:40:34.000Z","size":1886,"stargazers_count":85,"open_issues_count":19,"forks_count":35,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-28T06:08:38.680Z","etag":null,"topics":["aws","dynamodb","dynamodbmapper","java","kotlin"],"latest_commit_sha":null,"homepage":"https://cashapp.github.io/tempest/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cashapp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"docs/code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-08T21:10:09.000Z","updated_at":"2025-03-17T13:33:05.000Z","dependencies_parsed_at":"2023-12-15T00:27:22.254Z","dependency_job_id":"7478af25-380c-47d6-9e52-4861e2e71745","html_url":"https://github.com/cashapp/tempest","commit_stats":{"total_commits":186,"total_committers":32,"mean_commits":5.8125,"dds":0.5967741935483871,"last_synced_commit":"cec84f607b30683d435eb092843c79846ae83fc3"},"previous_names":[],"tags_count":45,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cashapp%2Ftempest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cashapp%2Ftempest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cashapp%2Ftempest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cashapp%2Ftempest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cashapp","download_url":"https://codeload.github.com/cashapp/tempest/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247135139,"owners_count":20889420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","dynamodb","dynamodbmapper","java","kotlin"],"created_at":"2024-12-19T03:17:25.711Z","updated_at":"2025-04-04T07:04:04.979Z","avatar_url":"https://github.com/cashapp.png","language":"Kotlin","readme":"# Tempest\nTypesafe DynamoDB for Kotlin and Java.\n\nSee the [project website](https://cashapp.github.io/tempest) for documentation and APIs.\n\n## Efficient DynamoDB\n\nDynamoDB applications perform best (and cost the least to operate!) when data is organized for locality:\n\n* **Multiple types per table**: The application can store different entity types in a single table. DynamoDB schemas are flexible.\n* **Related entities are stored together**: Entities that are accessed together should be stored together. This makes it possible to answer common queries in as few requests as possible, [ideally one](https://www.alexdebrie.com/posts/dynamodb-single-table/#the-solution-pre-join-your-data-into-item-collections).\n\n### Example\n\nLet's build a music library with the following features:\n\n* Fetching multiple albums, each of which contains multiple tracks.\n* Fetching individual tracks.\n\nWe express it like this in code:\n\n#### Kotlin\n\n```kotlin\ninterface MusicLibrary {\n  fun getAlbum(key: AlbumKey): Album\n  fun getTrack(key: TrackKey): Track\n}\n\ndata class Album(\n  val album_title: String,\n  val album_artist: String,\n  val release_date: String,\n  val genre: String,\n  val tracks: List\u003cTrack\u003e\n)\n\ndata class Track(\n  val track_title: String,\n  val run_length: String\n)\n```\n\n#### Java\n\n```java\npublic interface MusicLibrary {\n  Album getAlbum(AlbumKey key);\n  Track getTrack(TrackKey key); \n}\n\npublic class Album {\n  public final String album_title;\n  public final String album_artist;\n  public final String release_date;\n  public final String genre;\n  public final List\u003cTrack\u003e tracks; \n}\n\npublic class Track(\n  public final String track_title;\n  public final String run_length;\n)\n```\n\nWe optimize for this access pattern by putting albums and tracks in the same table:\n\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd colspan=2 align=\"center\"\u003ePrimary Key\u003c/td\u003e\n      \u003ctd rowspan=2 colspan=4 align=\"center\" valign=\"middle\"\u003eAttributes\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003e\u003cstrong\u003epartition_key\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003esort_key\u003c/strong\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003c!-- Note: It is important to declare both vertical-align and valign here. \n           vertical-align only works in the project website \n           while valign only works in Github's formatting for README.md. --\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_1\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eINFO\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003ealbum_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003ealbum_artist\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erelease_date\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003egenre\u003c/strong\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eThe Dark Side of the Moon\u003c/td\u003e\n      \u003ctd\u003ePink Floyd\u003c/td\u003e\n      \u003ctd\u003e1973-03-01\u003c/td\u003e\n      \u003ctd\u003eProgressive rock\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_1\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eTRACK_1\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003etrack_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erun_length\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eSpeak to Me\u003c/td\u003e\n      \u003ctd\u003ePT1M13S\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_1\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eTRACK_2\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003etrack_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erun_length\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eBreathe\u003c/td\u003e\n      \u003ctd\u003ePT2M43S\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_1\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eTRACK_3\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003etrack_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erun_length\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eOn the Run\u003c/td\u003e\n      \u003ctd\u003ePT3M36S\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd colspan=6\u003e...\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_2\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eINFO\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003ealbum_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003ealbum_artist\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erelease_date\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003egenre\u003c/strong\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eThe Wall\u003c/td\u003e\n      \u003ctd\u003ePink Floyd\u003c/td\u003e\n      \u003ctd\u003e1979-11-30\u003c/td\u003e\n      \u003ctd\u003eProgressive rock\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eALBUM_2\u003c/td\u003e\n      \u003ctd rowspan=2 style=\"vertical-align:bottom;\" valign=\"bottom\"\u003eTRACK_1\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003etrack_title\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u003cstrong\u003erun_length\u003c/strong\u003e\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eIn the Flesh?\u003c/td\u003e\n      \u003ctd\u003ePT3M20S\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n      \u003ctd\u003e\u0026nbsp;\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd colspan=6\u003e...\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\nThis table uses a [composite primary key](https://aws.amazon.com/blogs/database/choosing-the-right-dynamodb-partition-key/), `(parition_key, sort_key)`, to identify each item.\n\n* The key `(\"ALBUM_1\", \"INFO\")` identifies `ALBUM_1`'s metadata.\n* The key `(\"ALBUM_1\", \"TRACK_1\")` identifies `ALBUM_1`'s first track.\n\nThis table stores tracks belonging to the same album together and sorts them by the track number. The application needs only one request to DynamoDB to get the album and its tracks.\n\n```\naws dynamodb query \\\n    --table-name music_library_items \\\n    --key-conditions '{ \n        \"PK\": { \n            \"ComparisonOperator\": \"EQ\",\n            \"AttributeValueList\": [ { \"S\": \"ALBUM_1\" } ]\n        } \n    }'\n```\n\n## Why Tempest?\n\nFor locality, we smashed together several entity types in the same table. This improves performance! But it breaks type safety in DynamoDBMapper.\n\n### DynamoDBMapper API\n\n[`DynamoDBMapper`](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.html) / [`DynamoDbEnhancedClient`](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/DynamoDbEnhancedClient.html), the official Java API, forces you to write weakly-typed code that models the actual persistence type.\n\n#### Kotlin\n\n```kotlin\n// NOTE: This is not Tempest! It is an example used for comparison.\n@DynamoDBTable(tableName = \"music_library_items\")\nclass MusicLibraryItem {\n  // All Items.\n  @DynamoDBHashKey\n  var partition_key: String? = null\n  @DynamoDBRangeKey\n  var sort_key: String? = null\n\n  // AlbumInfo.\n  @DynamoDBAttribute\n  var album_title: String? = null\n  @DynamoDBAttribute\n  var album_artist: String? = null\n  @DynamoDBAttribute\n  var release_date: String? = null\n  @DynamoDBAttribute\n  var genre: String? = null\n\n  // AlbumTrack.\n  @DynamoDBAttribute\n  var track_title: String? = null\n  @DynamoDBAttribute\n  var run_length: String? = null\n}\n```\n\n#### Java\n\n```java\n// NOTE: This is not Tempest! It is an example used for comparison.\n@DynamoDBTable(tableName = \"music_library_items\")\npublic class MusicLibraryItem {\n  // All Items.\n  String partition_key = null;\n  String sort_key = null;\n\n  // AlbumInfo.\n  String album_title = null;\n  String artist_name = null;\n  String release_date = null;\n  String genre_name = null;\n\n  // AlbumTrack.\n  String track_title = null;\n  String run_length = null;\n\n  @DynamoDBHashKey(attributeName = \"partition_key\")\n  public String getPartitionKey() {\n    return partition_key;\n  }\n\n  public void setPartitionKey(String partition_key) {\n    this.partition_key = partition_key;\n  }\n\n  @DynamoDBRangeKey(attributeName = \"sort_key\")\n  public String getSortKey() {\n    return sort_key;\n  }\n\n  public void setSortKey(String sort_key) {\n    this.sort_key = sort_key;\n  }\n\n  @DynamoDBAttribute(attributeName = \"album_title\")\n  public String getAlbumTitle() {\n    return album_title;\n  }\n\n  public void setAlbumTitle(String album_title) {\n    this.album_title = album_title;\n  }\n\n  @DynamoDBAttribute(attributeName = \"artist_name\")\n  public String getArtistName() {\n    return artist_name;\n  }\n\n  public void setArtistName(String artist_name) {\n    this.artist_name = artist_name;\n  }\n\n  @DynamoDBAttribute(attributeName = \"release_date\")\n  public String getReleaseDate() {\n    return release_date;\n  }\n\n  public void setReleaseDate(String release_date) {\n    this.release_date = release_date;\n  }\n\n  @DynamoDBAttribute(attributeName = \"genre_name\")\n  public String getGenreName() {\n    return genre_name;\n  }\n\n  public void setGenreName(String genre_name) {\n    this.genre_name = genre_name;\n  }\n\n  @DynamoDBAttribute(attributeName = \"track_title\")\n  public String getTrackTitle() {\n    return track_title;\n  }\n\n  public void setTrackTitle(String track_title) {\n    this.track_title = track_title;\n  }\n\n  @DynamoDBAttribute(attributeName = \"run_length\")\n  public String getRunLength() {\n    return run_length;\n  }\n\n  public void setRunLength(String run_length) {\n    this.run_length = run_length;\n  }\n}\n```\n\nNote that `MusicLibraryItem` is a union type of all the entity types: `AlbumInfo` and `AlbumTrack`. Because all of its attributes are nullable and mutable, code that interacts with it is brittle and error prone.\n\n### Tempest API\n\nTempest restores maintainability without losing locality. It lets you declare strongly-typed key and item classes for each logical type in the domain layer.\n\n#### Kotlin\n    \n```kotlin\ndata class AlbumInfo(\n  @Attribute(name = \"partition_key\")\n  val album_token: String,\n  val album_title: String,\n  val album_artist: String,\n  val release_date: String,\n  val genre_name: String\n) {\n  @Attribute(prefix = \"INFO_\")\n  val sort_key: String = \"\"\n\n  data class Key(\n    val album_token: String\n  ) {\n    val sort_key: String = \"\"\n  }\n}\n\ndata class AlbumTrack(\n  @Attribute(name = \"partition_key\")\n  val album_token: String,\n  @Attribute(name = \"sort_key\", prefix = \"TRACK_\")\n  val track_token: String,\n  val track_title: String,\n  val run_length: String\n) {\n  data class Key(\n    val album_token: String,\n    val track_token: String\n  )\n}\n```\n\n#### Java\n\n```java\npublic class AlbumInfo {\n  @Attribute(name = \"partition_key\")\n  public final String album_token;\n  public final String album_title;\n  public final String artist_name;\n  public final String release_date;\n  public final String genre_name;\n\n  @Attribute(prefix = \"INFO_\")\n  public final String sort_key = \"\";\n\n  public static class Key {\n    public final String album_token;\n    public final String sort_key = \"\";\n  }\n}\n\npublic class AlbumTrack {\n  @Attribute(name = \"partition_key\")\n  public final String album_token;\n  @Attribute(name = \"sort_key\", prefix = \"TRACK_\")\n  public final String track_token;\n  public final String track_title;\n  public final String run_length;\n\n  public static class Key {\n    public final String album_token;\n    public final String track_token;\n  }\n}\n```\n\nYou build business logic with logical types. Tempest handles mapping them to the underlying persistence type.\n\n\u003e Note: The base item type `MusicLibraryItem` is still used for the `LogicalTable`. This type is intended to model an empty row, so all its fields should be nullable with a `null` default value. Using non-nullable types or fields with default values will cause issues during serialization and querying.\n\n#### Kotlin\n\n```kotlin\ninterface MusicLibraryTable : LogicalTable\u003cMusicLibraryItem\u003e {\n  val albumInfo: InlineView\u003cAlbumInfo.Key, AlbumInfo\u003e\n  val albumTracks: InlineView\u003cAlbumTrack.Key, AlbumTrack\u003e\n}\n\nprivate val musicLibrary: MusicLibraryTable\n\n// Load.\nfun getAlbumTitle(albumToken: String): String? {\n  val key = AlbumInfo.Key(albumToken)\n  val albumInfo = musicLibrary.albumInfo.load(key) ?: return null\n  return albumInfo.album_title\n}\n\n// Update.\nfun addAlbumTrack(\n  albumToken: String, \n  track_token: String, \n  track_title: String, \n  run_length: String\n) {\n  val newAlbumTrack = AlbumTrack(albumToken, track_token, track_title, run_length)\n  musicLibrary.albumTracks.save(newAlbumTrack)\n} \n\n// Query.\nfun getAlbumTrackTitles(albumToken: String): List\u003cString\u003e {\n  val page = musicLibrary.albumTracks.query(\n    keyCondition = BeginsWith(AlbumTrack.Key(albumToken))\n  )\n  return page.contents.map { it.track_title }\n}\n```\n\n#### Java\n\n```java\npublic interface MusicLibraryTable extends LogicalTable\u003cMusicLibraryItem\u003e {\n  InlineView\u003cAlbumInfo.Key, AlbumInfo\u003e albumInfo();\n  InlineView\u003cAlbumTrack.Key, AlbumTrack\u003e albumTracks();\n}\n\nprivate MusicLibraryTable musicLibrary; \n\n// Load.\n@Nullable\npublic String getAlbumTitle(String albumToken) {\n  AlbumInfo albumInfo = table.albumInfo().load(new AlbumInfo.Key(albumToken));\n  if (albumInfo == null) {\n    return null;\n  }\n  return albumInfo.album_title;\n}\n\n// Update.\npublic void addAlbumTrack(\n  String albumToken, \n  String track_token, \n  String track_title, \n  String run_length\n) {\n  AlbumTrack newAlbumTrack = new AlbumTrack(albumToken, track_token, track_title, run_length);\n  musicLibrary.albumTracks().save(newAlbumTrack);\n}\n\n// Query.\npublic List\u003cString\u003e getAlbumTrackTitles(String albumToken) {\n  Page\u003cAlbumTrack.Key, AlbumTrack\u003e page = musicLibrary.albumTracks().query(\n      // keyCondition.\n      new BeginsWith\u003c\u003e(\n          // prefix.\n          new AlbumTrack.Key(albumToken)\n      )\n  );\n  return page.getContents().stream().map(track -\u003e track.track_title).collect(Collectors.toList());\n}\n```\n\n## Get Tempest\n\nFor AWS SDK 1.x:\n\n```groovy\nimplementation \"app.cash.tempest:tempest:1.10.0\"\n```\n\nFor AWS SDK 2.x:\n\n```groovy\nimplementation \"app.cash.tempest:tempest2:1.10.0\"\n```\n\n## Migrating From Tempest 1 to Tempest 2\n\nPlease follow the [Migration Guide](guide/v2_upgrade_guide) that has been set up to upgrade from Tempest 1 (AWS SDK 1.x) to Tempest 2 (AWS SDK 2.x)\n\n## License\n\n    Copyright 2020 Square, Inc.\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcashapp%2Ftempest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcashapp%2Ftempest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcashapp%2Ftempest/lists"}