AWS DynamoDB is a key-value database designed to run high-performance applications at any scale. It automatically scales up and down based on your current traffic, and does not require maintaining connections (as requests are sent over HTTP), which makes it the go-to DB service for serverless developers on AWS.
Because it’s 2023 and no-one writes HTTP requests anymore, AWS published a SDK called the document client to craft said requests. However, if you’ve ever used it, you will know that it’s still very painful to use.
For instance, let’s look at this UpdateCommand
example from the DynamoDB docs itself:
await DocumentClient.send(
new UpdateCommand({
TableName: 'TABLE_NAME',
Key: {
// 👇 No type-safety on the primary key
title: 'MOVIE_NAME',
year: 'MOVIE_YEAR',
},
// 👇 String expressions hard to build (and still no type-safety)
UpdateExpression: 'set info.plot = :p, info.#r = :r',
// 👇 When used in Expressions, attribute names have to be provided separately
ExpressionAttributeNames: {
'#r': 'rank',
},
// 🤦♂️ List of attribute names as strings separated by commas
ProjectionExpression: '#r',
// 👇 Attribute values have to be provided separately
ExpressionAttributeValues: {
// 👇 No validation or type-safety to enforce DB schema
':p': 'MOVIE_PLOT',
':r': 'MOVIE_RANK',
},
}),
);
It is a very simple example (updating two fields of a Movie
item), yet already very verbose 😅 And things only get messier as the complexity of your business logic grows: What if your items have 20 attributes? With some of them deeply nested? Or optional? What if you want to index an item or not depending on its values (e.g. a status
attribute)? What about polymorphism?
In those cases (which are quite common) the required code to generate those requests can get very hard to maintain. That is why, very early on, developers built open-source libraries to “wrap” the DynamoDB client, with two goals in mind:
- 🏋️♀️ Simplifying the writing of DynamoDB requests
- 📐 Adding run-time data validation, i.e. “artificial” schemas to a schema-less DB (and more recently type-safety)
For instance, here is an example of the same UpdateCommand
with one of those wrappers, DynamoDB-Toolbox:
import { Table, Entity } from 'dynamodb-toolbox';
// Provided some schema specifications...
const MovieTable = new Table({
name: 'TABLE_NAME',
partitionKey: 'title',
sortKey: 'year',
DocumentClient, // <= the original DocumentClient
});
const MovieEntity = new Entity({
name: 'Customer',
attributes: {
title: { partitionKey: true, type: 'string' },
year: { sortKey: true, type: 'string' },
info: { type: 'map' },
},
table: MovieTable,
} as const);
// ...we get a validated AND type-safe request method 🙌
await MovieEntity.update({
title: 'MOVIE_NAME',
year: 'MOVIE_YEAR',
info: {
plot: 'MOVIE_PLOT',
rank: 'MOVIE_RANK',
},
});
And just like that, we went from an obscure 18-line object to a readable and elegant 8-liner 🤩 Not bad, don't you think?
DynamoDB-Toolbox is not the only SDK wrapper out there. If you browse Alex DeBrie’s awesome-dynamodb-tools repo, you’ll actually find a bunch of them. So, which one should you chose?
In this article, we took an in depth comparison of the 4 most popular DynamoDB wrappers:
- 🦌 Dynamoose: Dynamoose is the OG of DynamoDB wrappers. Created in 2015, it provides a syntax that closely mirrors that of the popular Mongoose library for MongoDB.
- 🧰 DynamoDB Toolbox: DynamoDB-Toolbox was created by Jeremy Daly, AWS Serverless Hero, and writer of the newsletter off-by-none. It was first released in September 2018 and has gained in popularity ever since.
- ⚡️ ElectroDB: Released in April 2020 by Tyler W. Walsh, ElectroDB benefits from having been created after Typescript won over the JS ecosystem. So it has a higher focus on type-safety than its predecessors.
- 💍 DynamoDB-OneTable: First released in January 2021, DynamoDB-OneTable is maintained by Sensedeep and is part of its broader Serverless Developer Studio offer.
We ranked them based on the following criteria:
- 📣 Library state: Classic open-source KPIs such as number of downloads, community, documentation etc.
- 🏗️ Data modeling: The broadness of their Entity definition API. Do they allow attribute name re-mapping (useful for single-table design)? Do they support enums? Nested attributes definitions? Computing indexes from other attributes?
- ✨ Typescript support: Type-safety is all the rage these days! All libraries come with some sort of type-safety, but type-inference (i.e. inferring tailored types from custom schemas) is still hard to get right.
- 🤖 API: How easy it is to do common DynamoDB requests like
put
,get
orquery
... with secondary indexes, filters and conditions (You can find examples for each wrapper in our dedicated repo).
📣 Library state
(*as of 2023/02) | 🦌 Dynamoose | 🧰 DynamoDB-Toolbox | ⚡️ ElectroDB | 💍 DynamoDB-OneTable |
---|---|---|---|---|
First release date | 2014-02-27 | 2019-11-20 | 2020-03-11 | 2021-01-12 |
Last release date | ✅ Jan. 6, 2023 | ✅ Jan. 8, 2023 | ✅ Jan. 20, 2023 | ✅ Jan. 25, 2023 |
Github ⭐️ | 1900 | 1400 | 530 | 505 |
NPM weekly downloads | 86 k | 38 k | 4 k | 16 k |
Bundle size | 🟡 382 kB | ✅ 64.1 kB | 🟡 176.7 kB | ✅ 64.3 kB |
Documentation | ✅ | ✅ | 🟡 No global search | ❌ Can be improved |
DynamoDB Client v3 compatibility | ✅ | ❌ | ✅ | ✅ |
All four libraries are well maintained, and have enough GitHub stars and npm downloads to be considered “battle tested”.
Dynamoose has the highest stats (probably from being the first one around). However, it’s the heaviest, with a size of 382KB, which is not negligible, considering that bundles above 5MB negatively impact Lambdas cold starts.
The main takeaways are that DynamoDB-Toolbox is not compatible with the V3 of the DynamoDB client (though it should be coming soon), and that the DynamoDB-OneTable documentation leaves to be desired.
🏗️ Data modeling
We initially started with a very broad scope of features useful for Entity definition (like specifying attributes as required, or aliasing attributes). However, most of them were already implemented by all libraries. For the sake of simplicity, we removed them and kept the following ones:
-
Nested attributes definition: Could we type nested fields of lists and maps attributes (like
plot
andrank
in our firstMovie
example)? - Enum support: Could we specify a finite range of values for a primitive attribute?
- Default values: Could we provide default values for an attribute? That is especially useful for entities with “simple” access patterns like fixed strings. We differentiated independent defaults (fixed or derived from context such as timestamps or env variables) from dependent defaults (computed from other attributes).
- Pre-save/post-fetch attribute transformation: This can be needed for technical reasons, such as prefixing attributes. When possible, it’s best to hide such details from your code and let your wrapper handle the heavy-lifting.
- Polymorphism support: Sometimes, items can switch between different statuses and shapes that go with them. We tested how easy it was to translate to in each library.
🦌 Dynamoose | 🧰 DynamoDB-Toolbox | ⚡️ ElectroDB | 💍 DynamoDB-OneTable | |
---|---|---|---|---|
Nested attributes definition | ✅ | ❌ | ✅ | ✅ |
Enum support | ✅ | ❌ | ✅ | ✅ |
Independent defaults | ✅ | ✅ | ✅ | ✅ |
Dependent defaults | ❌ | ✅ | 🟡 Via the set option |
🟡 Via string templates like "user#${email}"
|
Attribute value transformation | ✅ | ✅ | ✅ | ❌ |
Polymorphism | ❌ | ❌ | ❌ | ❌ |
Overall, Dynamoose and ElectroDB have the upper hand, with ElectroDB being slightly ahead as it allows deriving attributes default values from other attributes.
Surprisingly, none of those libraries handles polymorphism and type-safe dependent defaults. As a maintainer of DynamoDB-Toolbox, I know for sure that those features are coming in the next major, so if you’re already using it, do not consider migrating to ElectroDB just yet 🙂
✨ Typescript support
All libraries support Typescript at a basic level, so we mostly focused on type inference. We looked for type inference in:
- DynamoDB requests: Root and nested level attributes
- Dependent defaults definition
- Expressions: Conditions, filters and projections
🦌 Dynamoose | 🧰 DynamoDB-Toolbox | ⚡️ ElectroDB | 💍 DynamoDB-OneTable | |
---|---|---|---|---|
Requests (Root attributes) | ❌ | ✅ | ✅ | ✅ |
Requests (Nested attributes) | ❌ | ❌ | ✅ | ✅ |
Dependent defaults | ❌ | ❌ | ❌ | ❌ |
Expressions | ❌ | 🟡 yes but only at root level | ✅ | ❌ Via string templates like "(${role} = {admin})"
|
IDE performances | ❌ | ❌ Slow | ✅ | ✅ |
Once again ElectroDB has the upper hand here. Very nice job, Tyler W. Walsh 😎
🤖 API
Finally we compared the APIs of each solution for sending DynamoDB requests, i.e. how easy they were to write and understand (verbosity, complexity etc).
-
Single Item Requests: For usual
PUT
,GET
andDELETE
requests. -
Updates: For
UPDATE
requests. We tested updating nested fields, adding values to numbers and elements to lists. - Queries: For querying and using secondary indexes.
- Conditions & filters: For conditioned write operations and filtered query results.
🦌 Dynamoose | 🧰 DynamoDB-Toolbox | ⚡️ ElectroDB | 💍 DynamoDB-OneTable | |
---|---|---|---|---|
Single Item Requests | 🟡 Clunky | ✅ | ✅ | 🟡 Hard to override an existing item (PUT ) |
Updates | 🟡 Clunky | 🟡 Operations ($delete , $add ...) mixed with attributes |
✅ | 🟡 Operations (delete , add ...) are far from attributes (in options ) |
Queries | ❌ Uses only FilterExpression | 🟡 Confusing query formalism | ✅ | ✅ |
Conditions & filters | 🟡 Verbose, ORM-ish | 🟡 Confusing OR clauses |
🟡 Easy to use but incomplete (e.g. AND /OR clauses) |
❌ String template 🙄 |
If the previous section wasn't enough, this one should convince you NOT to use Dynamoose: I found it hard to believe, but it doesn't even support basic (efficient) queries, and uses only filter expressions under the hood 🙄.
That being said, ElectroDB has a the nicest API. I especially liked the possibility to name queries according to their business meaning, but not the find
and match
methods, which are not native DynamoDB requests and can build costly and inefficient scan
requests without you being aware.
Conclusion
Overall, as of march 2023, ElectroDB looks like the best DynamoDB client wrapper. Although it is newer and less “battle tested”, it is better than its concurrent on every other criteria: It has the same data modeling features, a more complete type inference, and a nicer API.
Nevertheless, there are some parts that could be improved:
- Type inference still has some blind spots
- There's no support for polymorphism
- Entity definition autocompletion could be more helpful (it would benefit from a zod-like approach)
Finally, I would not rule out DynamoDB-Toolbox just yet! Its next major is just around the corner, with many new capabilities that even ElectroDB doesn’t have (such as type-safe dependent defaults and polymorphism). So expect a round 2 of this article in the next few months… 😉
Top comments (4)
I've tried the official AWS SDK, dynamoose and electrodb. So far, the electrodb is an abosolute go-to library for me, simply because it shows a good balance between low and high level, it's less buggy and the single-table philosophy support is quite good. And of course, the TS type safety is vital too!
If not for my long love for mongoose, I guess I would never had even tried its mocker dynamoose. Strip it off this mongoose vibe and it is just a buggy lib when it comes to single tables
Nice article @thomasaribart! Just throwing it out there, but if you've struggled with these libraries, check out Dynamode, a DynamoDB client I wrote last year. It makes working with DynamoDB way easier. Give it a look!
Thanks for this! Love that you keep it brief but informative.
Decided to go with ElectroDB because of the superior TS support. Might try out DynamoDB-toolbox in the future.