Event-driven side effects and automatic cleanup are core building blocks of serverless DynamoDB applications.
DynamoDB Streams records every item-level change (insert, modify, remove) in order per item, retained for 24 hours. The StreamViewType decides what each record carries: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE or NEW_AND_OLD_IMAGES.
The usual consumer is AWS Lambda through an event source mapping: Lambda polls the stream and invokes your function with batches of records. Use it to update aggregates, keep a search index in sync, send notifications, or replicate to another store. Records for the same item arrive in order; a function that throws will be retried with the same batch, so handlers must be idempotent, and you should configure a failure destination and BisectBatchOnFunctionError so one bad record cannot block a shard. Alternatively stream changes to Kinesis Data Streams for longer retention and more consumers.
Time to Live (TTL): choose an attribute holding an expiry time as a Unix epoch number in seconds. After that time DynamoDB deletes the item in the background at no write cost, typically within a few days, not instantly. Filter out expired-but-present items in queries (expires_at > :now) when it matters. TTL deletions appear in the stream with a userIdentity of dynamodb.amazonaws.com, so you can archive expired items to S3.
Example
# Lambda handler attached to the Orders table stream (NEW_AND_OLD_IMAGES)
import time
def handler(event, context):
for rec in event["Records"]:
name = rec["eventName"] # INSERT | MODIFY | REMOVE
keys = rec["dynamodb"]["Keys"] # wire format: {"customer_id": {"S": "c1"}, ...}
if name == "MODIFY":
old = rec["dynamodb"]["OldImage"]["status"]["S"]
new = rec["dynamodb"]["NewImage"]["status"]["S"]
if old != new == "SHIPPED":
print("notify", keys["customer_id"]["S"]) # must be idempotent
elif name == "REMOVE" and rec.get("userIdentity", {}).get(
"principalId") == "dynamodb.amazonaws.com":
print("expired by TTL", keys)
# writing an item that expires in 7 days (TTL attribute "expires_at")
item = {"customer_id": "c1", "order_id": "cart",
"expires_at": int(time.time()) + 7 * 24 * 3600}Remember
- Streams keep 24 hours of ordered item changes; Lambda is the usual consumer
- Stream handlers are retried, so make them idempotent
- TTL takes an epoch-seconds number and deletes expired items for free
- TTL deletion is delayed; filter expired items if exactness matters
Watch out for
- Storing the TTL attribute as an ISO date string or milliseconds; TTL then never deletes the item.