From Model to Implementation: DeBrie's Practical Rules for Building the Table
Objective
Understand the six concrete rules Alex DeBrie gives for turning a finished access-pattern-first entity-relationship model — the output of the process in DynamoDB Data Modeling: An Access-Patterns-First Approach — into working application code and an actual CreateTable call: keeping indexing attributes separate from application attributes, pushing all DynamoDB-specific code to the boundary of the application, never sharing an attribute across two different indexes, tagging every item with its entity type, writing small debug scripts per access pattern, and — only for the largest tables — shortening attribute names.
Use Cases
- Reviewing a pull request that adds a data-access layer for a new DynamoDB-backed service, and checking whether the raw
GetItem/PutItemcalls leak past the repository/data module into business logic — the book's boundary rule made concrete. - Deciding, after finishing the entity chart and key design from
dynamodb-data-modeling-approach, whether to reuse the sort key value as a GSI sort key too ("just pointGSI1atPKandSK") to save a few bytes per item — and knowing why the book says not to. - Setting up a background ETL / migration script (see
dynamodb-migration-strategies-for-single-table-design) and needing a cheap way to select only the entity type being migrated out of a single overloaded table. - Debugging a production incident where a
Queryagainst a GSI returns the wrong items, and needing a fast, repeatable way to reproduce the access pattern from the command line instead of re-clicking through the AWS console each time. - Estimating storage cost for a table projected to hold billions of items, and deciding whether attribute-name shortening is worth the added complexity at your actual scale — versus a smaller table where it plainly isn't.
- Writing the actual
CreateTablecall (or its CDK/Terraform/CloudFormation equivalent) for a model that was designed on paper, and wanting a short pre-flight checklist rather than guessing at attribute types.
Deep Dive
Where this chapter sits: after the model, before the code
The book is explicit about the ratio of effort: "90% of the work of using DynamoDB happens in the planning stage, before you write a single line of code." That planning stage — the ERD, the access-pattern chart, the primary-key design — is dynamodb-data-modeling-approach. This chapter is the other 10%: "At some point, however, you need to move from model to implementation. This chapter includes guidance on how to implement your DynamoDB data model in your application code." It's a checklist for the moment the paper model becomes a CreateTable call and a data-access layer, not a modeling technique in its own right.
Rule 1 — Separate application attributes from indexing attributes
An item pulled straight off a single-table design looks like this:
plaintext{ "PK": { "S": "USER#alexdebrie" }, "SK": { "S": "USER#alexdebrie" }, "GSI1PK": { "S": "ORG#facebook" }, "GSI1SK": { "S": "USER#alexdebrie" }, "Username": { "S": "alexdebrie" }, "FirstName": { "S": "Alex" }, "LastName": { "S": "DeBrie" }, "OrganizationName": { "S": "Facebook" } }
DeBrie draws a hard line between the first four attributes and the rest: "the first four attributes are all related to my DynamoDB data model but have no meaning in my application business logic. I refer to these as 'indexing attributes,' as they're only there for indexing your data in DynamoDB." Username, FirstName, and the rest are application attributes — the things the business logic actually cares about.
The dependency only runs one direction. Application attributes are allowed to inform indexing attributes — here Username fills in the USER# template for PK, SK, and GSI1SK — but not the reverse: "I recommend against going the other way. Don't think that you can remove the Username attribute from your item since it's already encoded into your PK. It adds complexity and risks data loss if you change your data model and indexing attributes in the future." The book accepts the storage cost of this duplication explicitly: "It will result in slightly larger item sizes due to duplicated data, but I think it's worth it."
Rule 2 — Implement your data model at the very boundary of your application
The raw item above has two problems for anyone writing business logic against it: it's cluttered with indexing attributes that mean nothing outside DynamoDB, and every value is wrapped in a type-descriptor map ({"S": "..."}). DeBrie's fix is architectural, not cosmetic — push all of that translation to one narrow layer:
plaintextdef get_user(username): resp = client.get_item( TableName='AppTable', Key={ 'PK': { 'S': f'USER#{username}' }} ) return User( username=resp['Item']['Username']['S'], first_name=resp['Item']['FirstName']['S'], last_name=resp['Item']['LastName']['S'], )
The core of the application only ever sees data.get_user(username='alexdebrie') returning a plain User object — it never constructs a PK, never unwraps a {"S": ...} map, never knows a GSI1PK exists. "All interaction with DynamoDB should be handled in the data module that is at the boundary of your application... Write that DynamoDB logic once, at the edge of your application, and operate on application objects the rest of the time." This is the same boundary idea a repository or DAO layer gives you over a relational database — the difference is how much translation work happens at that boundary, because DynamoDB's key-templating and type-wrapping have no equivalent in a plain SQL row.
Rule 3 — Don't reuse attributes across multiple indexes
Looking at the same item again, SK and GSI1SK hold the identical value USER#alexdebrie. The obvious "optimization" is to stop writing GSI1SK at all and define GSI1 with a key schema of GSI1PK + SK — reusing the base table's sort key as the index's sort key. DeBrie is blunt: "Don't do this."
The saved storage is real but small, and the cost is structural: "it will make your data modeling more difficult. If you have multiple entity types in your application, you'll tie yourself in knots trying to make the attributes work across multiple different indexes. Further, if you do need to add additional access patterns or migrate data, this will make it more difficult." His replacement rule is mechanical and easy to enforce in review: "For each global secondary index you use, give it a generic name of GSI<Number>. Then, use GSI<Number>PK and GSI<Number>SK for your attribute types." One index, one dedicated pair of attributes, always — no cross-wiring, even when two values happen to coincide today.
Rule 4 — Add a Type attribute to every item
Because entity type in a single-table design lives inside the value of PK/SK (USER#... vs. ORDER#...), it's not something you can filter on cheaply or eyeball at a glance. DeBrie's fix is a plain string attribute written onto every item:
plaintextdef save_user(user: User): resp = client.put_item( TableName='AppTable', Item={ 'PK': { 'S': f'USER#{User.username}' }, 'SK': { 'S': f'USER#{User.username}' }, 'GSI1PK': { 'S': f'ORG#{User.org_name}' }, 'GSI1SK': { 'S': f'USER#{User.username}' }, 'Type': { 'S': 'User' }, 'Username': { 'S': User.username }, 'FirstName': { 'S': User.first_name}, 'LastName': { 'S': User.last_name}, } ) return user
He gives three concrete reasons to bother, beyond just orienting yourself in the AWS console:
- Migrations. The strategies in
dynamodb-migration-strategies-for-single-table-designoften require a background ETL job that scans the table and decorates existing items with new indexing attributes — but usually only for one entity type at a time. "I like to use a filter expression on the Type attribute when scanning my table to ensure I'm only getting the items that I need. It simplifies the logic in my ETL script." - Analytics exports. DynamoDB is not built for OLAP queries, so analytics means exporting to Redshift or S3/Athena — and a single table holding every entity type is exactly wrong shape for a relational analytics engine. "After your initial export of data, you'll want to 're-normalize' it by moving your different entity types into their own tables. Having a Type attribute makes it easier to write the transformation query and find the right items to move around."
- Console legibility. A quick, low-cost way to tell what you're looking at while debugging in the AWS console.
Rule 5 — Write scripts to help debug access patterns
Single-table design makes ad-hoc debugging harder than in a relational database: "your items are all jumbled together in the same table. You're accessing items from DynamoDB via indexed attributes rather than the application attributes that you're used to in your application. Finally, you may be using shortened attribute names that require translation to map it back to your actual application attribute names."
The recommended fix is a small CLI script per access pattern, sitting on top of the same data module from Rule 2:
plaintext# scripts/get_user.py import click import data @click.command() @click.option('--username', help='Username of user to retrieve.') def get_user(username): user = data.get_user(username) print(user) if __name__ == '__main__': get_user()
Run as python scripts/get_user.py --username alexdebrie, it prints the fetched User. For a single-item fetch this looks like overkill, but DeBrie's point is about the access patterns that aren't a single GetItem: "if you're retrieving multiple related items from a global secondary index with complex conditions on the sort key, these little scripts can be lifesavers. Write them at the same time you're implementing your data model." Writing the script alongside the data-access code, rather than after a bug report, is the actual habit being recommended.
Rule 6 — Shorten attribute names to save storage (advanced, largest tables only)
The last rule looks like it contradicts Rule 3, but it doesn't — it targets a different set of attributes. Rule 3 says never share an indexing attribute across indexes; Rule 6 says you may abbreviate application attribute names, because Rule 2 already guarantees nothing outside the data-access layer ever touches those names directly:
plaintextdef save_user(user: User): resp = client.put_item( TableName='AppTable', Item={ 'PK': { 'S': f'USER#{User.username}' }, 'SK': { 'S': f'USER#{User.username}' }, 'GSI1PK': { 'S': f'ORG#{User.org_name}' }, 'GSI1SK': { 'S': f'USER#{User.username}' }, 'u': { 'S': User.username }, 'fn': { 'S': User.first_name}, 'ln': { 'S': User.last_name}, } ) return user
Username becomes u, FirstName becomes fn. DeBrie is careful to flag this as opt-in, not a default: "this is a pretty advanced pattern that I would recommend only for the largest tables and for those that are heavily into the DynamoDB mindset... Because your application will never be touching these abbreviated names, it's safe to make these abbreviations." The rehydration step — turning fn back into first_name on read — belongs entirely inside the same boundary layer from Rule 2, so the rest of the application never sees the abbreviation. His closing scope note: "For the marginal application, the additional attribute names won't be a meaningful cost difference. However, if you plan on storing billions and trillions of items in DynamoDB, this can make a difference with storage."
Reading this as a pre-CreateTable checklist
Put together, the six rules resolve into a short pre-flight list before you actually call CreateTable (or write the CDK/Terraform/CloudFormation equivalent) for a model finished under dynamodb-data-modeling-approach:
- Every key attribute in your entity chart is named generically —
PK,SK,GSI1PK,GSI1SK,GSI2PK, ... — never after a specific entity's field. - No GSI's key schema reuses a base-table or another index's attribute (Rule 3) — each index gets its own dedicated
PK/SKpair, written explicitly on every item that belongs in it. - Every item type in the design carries a
Typeattribute in its write path. - There is exactly one module/package where
AttributeDefinitions,KeySchema, and every SDK call live — no other part of the codebase imports the DynamoDB client directly. - A debug script exists (or is planned) for each nontrivial access pattern, especially ones hitting a GSI with sort-key conditions.
- Attribute-name shortening is a deliberate, scale-driven decision, not a default — skip it unless the table is genuinely headed for billions of items.
Book vs. today
The mechanics described here are unchanged since 2020 — PK/SK overloading, GSI attribute isolation, and the boundary-layer pattern are exactly how the AWS SDKs and CreateTable still work. Two things around the edges of "actually writing the CreateTable call" are worth flagging:
AWS now actively steers new tables toward on-demand billing, which the book doesn't mention at all. The
CreateTableAPI'sBillingModeparameter still defaults toPROVISIONEDwhen omitted (in which caseProvisionedThroughputis required) — that hasn't changed. What has changed is the guidance: the current API reference states outright, "We recommend usingPAY_PER_REQUESTfor most DynamoDB workloads," and the AWS console now creates new tables in on-demand mode by default. In 2020, on-demand pricing was two years old and still a secondary option; today it's the recommended starting point, and the pre-flight checklist above should include "pickPAY_PER_REQUESTunless you already know you need provisioned capacity" alongside DeBrie's six rules. Rule 2's boundary pattern now has first-party tooling in some SDKs, but the discipline is still yours to enforce. The AWS SDK for Java v2's Enhanced DynamoDB Client and similar object-mapper layers in other SDKs can automate part of the type-wrapping/unwrapping Rule 2 describes by hand in Python. That reduces the boilerplate of the boundary layer but doesn't remove the need for one — a@DynamoDbBean-annotated class is still DynamoDB-shaped, and the discipline of never letting it leak past the data-access layer into business logic is exactly Rule 2, tooling or not.
Trade-offs
- The boundary-layer discipline (Rule 2) is easy to state and easy to erode. The first time someone needs "just one more field" from a
GetItemresponse in a hurry, the shortest path is to reach intoresponse['Item']['SomeAttr']['S']from inside a handler instead of extending the data module. Every instance of that shortcut is a place where changing the table's key design later requires hunting through business logic instead of touching one file. Treat any DynamoDB SDK import outside the designated data-access module as a code-review finding, not a style preference. - The generic
PK/SK/GSI<N>PKconvention (Rules 1 and 3) trades self-description for correctness under change. A table where key attributes are named after the convention is unreadable in the AWS console without the entity chart in hand — you cannot tell whatPK = "ORG#facebook"means without the mapping document. But the alternative — naming a GSI's keyUsernamebecause that's what it holds for one entity type — breaks the moment a second entity type needs that same index, which is precisely the scenario single-table design exists to support. The illegibility is the cost of keeping the design extensible; the entity chart fromdynamodb-data-modeling-approachis not optional documentation here, it's the only key. - The
Typeattribute is nearly free and easy to forget under time pressure. It costs a few bytes per item and one extra line in every write path, and DeBrie's three justifications (ETL filtering, analytics re-normalization, console legibility) don't pay off until a migration or an export actually happens — which can be months after the table went live. Skipping it at launch to save a line of code is the kind of decision that looks free right up until the first migration script has to be written without it, at which point recovering entity type requires parsing every item'sPKtemplate instead of one filter expression. - Debug scripts (Rule 5) are a real engineering investment competing with feature work, and the payoff is asymmetric by access pattern. For a plain
GetItemby primary key, a script barely earns its keep — you could just as easily use the AWS console or CLI. For aQueryagainst a GSI with abegins_withorbetweensort-key condition, reproducing the exact request by hand each time is slow and error-prone, and that's where the book's "lifesaver" claim holds. Writing scripts uniformly for every access pattern is wasted effort; skipping them for the multi-condition ones is where teams actually get burned during an incident. - Attribute-name shortening (Rule 6) is the one rule the book itself hedges hardest on, and that hedge should be taken seriously. The storage saved by turning
FirstNameintofnis genuinely marginal below billions of items, while the cost is permanent: every developer reading a raw item in the console or in logs now needs a name-mapping table in their head, and the rehydration logic is one more thing that can drift out of sync between read and write paths. Applying this rule on a table with a few million items is optimizing a cost that doesn't exist yet while paying a comprehension tax that does. - None of these six rules are enforced by DynamoDB itself. Nothing stops a
PutItemcall from writing an item without aTypeattribute, reusing aSKvalue as a GSI sort key, or bypassing the data-access module entirely — the table accepts whatever shape you send it. That's the same trade-off single-table design makes everywhere else: the flexibility that lets you overload a partition key to pre-join data is the same flexibility that lets a rushed change quietly violate every rule in this chapter. The checklist substitutes for a constraint the database won't give you, which is also why it belongs in code review, not just in a chapter you read once.
Documentation Links
- Alex DeBrie, "The DynamoDB Book", v1.0.1 (2020) — Chapter 9, "From modeling to implementation", p. 164-173
- AWS Documentation — CreateTable API Reference (BillingMode, KeySchema, GlobalSecondaryIndexes)
- AWS Documentation — Read/Write Capacity Mode (Provisioned vs. On-Demand)
- AWS Documentation — Best Practices for Using Secondary Indexes in DynamoDB
- AWS Documentation — DynamoDB Enhanced Client (AWS SDK for Java 2.x)
- AWS Documentation — Exporting DynamoDB Table Data to Amazon S3