A Clojure library for managing DynamoDB table migrations and keeping table definitions in sync between local and production environments.
Highly inspired by migratus and how easy it is to use. Dynatus provides a simple, declarative way to manage DynamoDB table schemas across environments.
Add to your deps.edn:
;; From Clojars
{:deps {org.clojars.morita/dynatus {:mvn/version "0.1.0-beta"}}}
;; Or from GitHub
{:deps {dynatus/dynatus {:git/url "https://github.com/MoritaHR/dynatus"
:git/sha "LATEST_SHA"}}}
(require '[dynatus.core :as dynatus]
'[cognitect.aws.client.api :as aws])
;; Create a DynamoDB client using AWS SDK
(def dynamo-client (aws/client {:api :dynamodb
:region "us-east-1"}))
;; Synchronize tables from EDN files in resources/dynamo
(dynatus/syncronizate {:client dynamo-client
:path "resources/dynamo"})
;; => {:sync true
;; :count 2
;; :migrated [{:table "users" :action :create}
;; {:table "orders" :action :create}]}
Dynatus provides two main functions for managing DynamoDB tables:
syncronizate - High-level synchronizationLoads table definitions from EDN files and synchronizes them with DynamoDB:
(dynatus/syncronizate {:client dynamo-client
:path "resources/dynamo"}) ; optional, defaults to "./resources/dynamo"
Returns:
{:sync true :count n :migrated [{:table "name" :action :create/:noop/:recreate}...]}{:sync false :reason "No DynamoDB migration files found." :path "..."}{:sync false :reason "Exception..." :error "error message"}execute-tables-sync - Low-level table operationsDirectly synchronizes a collection of table definitions:
(def table-defs
[{:TableName "users"
:KeySchema [{:AttributeName "user_id" :KeyType "HASH"}]
:AttributeDefinitions [{:AttributeName "user_id" :AttributeType "S"}]
:BillingMode "PAY_PER_REQUEST"}])
(dynatus/execute-tables-sync dynamo-client table-defs)
;; => [{:table "users" :action :create}]
Returns: Vector of {:table "name" :action :create/:noop/:recreate} for each table processed.
Create .edn files in your migrations directory:
;; resources/dynamo/users.edn
{:TableName "users"
:KeySchema [{:AttributeName "user_id"
:KeyType "HASH"}]
:AttributeDefinitions [{:AttributeName "user_id"
:AttributeType "S"}
{:AttributeName "email"
:AttributeType "S"}]
:BillingMode "PAY_PER_REQUEST"
:GlobalSecondaryIndexes [{:IndexName "email-index"
:KeySchema [{:AttributeName "email"
:KeyType "HASH"}]
:Projection {:ProjectionType "ALL"}}]
:StreamSpecification {:StreamEnabled true
:StreamViewType "NEW_AND_OLD_IMAGES"}
:Tags [{:Key "Environment"
:Value "production"}]}
For local development, configure your AWS client to connect to local DynamoDB:
(require '[cognitect.aws.client.api :as aws]
'[cognitect.aws.interceptors :as interceptors])
;; Define interceptor for local DynamoDB
(defmethod interceptors/modify-http-request "dynamodb"
[service http-request]
(-> http-request
(assoc :scheme :http
:server-port 8000
:server-name "localhost")
(assoc-in [:headers "host"] "localhost:8000")))
;; Create client - interceptor will redirect to local
(def local-client (aws/client {:api :dynamodb
:region "us-east-1"}))
;; Synchronize tables
(dynatus/syncronizate {:client local-client})
(require '[dynatus.test-fixtures :as fixtures])
(use-fixtures :each fixtures/with-dynamodb-container)
(deftest my-test
(testing "DynamoDB operations"
;; Use fixtures/*test-client* which is automatically connected
;; to a DynamoDB container
(let [client fixtures/*test-client*
result (dynatus/syncronizate {:client client
:path "test/resources/tables"})]
(is (= true (:sync result)))
;; Your test assertions here
)))
Dynatus performs three types of actions on tables:
:create - Table doesn't exist and will be created:noop - Table exists and matches the definition (no operation needed):recreate - Table exists but key schema has changed (requires recreation)All operations are idempotent - running syncronizate multiple times is safe:
;; First run - creates tables
(dynatus/syncronizate {:client client})
;; => {:sync true :count 2 :migrated [{:table "users" :action :create}...]}
;; Second run - no changes needed
(dynatus/syncronizate {:client client})
;; => {:sync true :count 2 :migrated [{:table "users" :action :noop}...]}
The library provides detailed error information:
;; Empty directory
(dynatus/syncronizate {:client client :path "empty/dir"})
;; => {:sync false
;; :reason "No DynamoDB migration files found."
;; :path "empty/dir"}
;; Connection error
(dynatus/syncronizate {:client bad-client})
;; => {:sync false
;; :reason "Exception while executing DynamoDB sync."
;; :error "Connection refused"}
# Run all tests
clojure -M:test -m kaocha.runner
# Run with specific test
clojure -M:test -m kaocha.runner --focus dynatus.core-test
# Run tests in watch mode
clojure -M:test -m kaocha.runner --watch
dynatus/
├── deps.edn # Dependencies
├── project.clj # Leiningen configuration
├── src/
│ └── dynatus/
│ ├── core.clj # Main API (syncronizate, execute-tables-sync)
│ ├── loader.clj # Table definition loader
│ ├── diff.clj # Table comparison logic
│ └── apply.clj # Apply migrations
├── test/
│ └── dynatus/
│ ├── core_test.clj # Integration tests
│ ├── test_fixtures.clj # Testcontainers setup
│ └── test_client.clj # Test-specific client with interceptors
└── resources/
└── dynamo/ # Table definitions
├── users.edn
└── orders.edn
syncronizate(syncronizate {:client aws-client
:path "path/to/edn/files"}) ; optional
High-level function that loads EDN files from a directory and synchronizes tables with DynamoDB.
Parameters:
:client - AWS DynamoDB client (required):path - Path to directory containing EDN files (optional, defaults to "./resources/dynamo")Returns:
{:sync true :count n :migrated [...]}{:sync false :reason "..." :error "..." :path "..."}execute-tables-sync(execute-tables-sync client table-definitions)
Low-level function that synchronizes a collection of table definitions.
Parameters:
client - AWS DynamoDB clienttable-definitions - Collection of table definition mapsReturns:
{:table "name" :action :create/:noop/:recreate}wait-for-table-active(wait-for-table-active client table-name)
Waits for a table to become active after creation.
Parameters:
client - AWS DynamoDB clienttable-name - Name of the table to wait forReturns:
true when table is activenil if timeout occursIN_DOCKER - Set to "true" when running in DockerDYNAMODB_LOCAL_ENDPOINT - Override local DynamoDB endpointAWS_PROFILE - AWS profile for credentials# Build the JAR file (using Leiningen)
lein jar
# Or using deps.edn
clojure -X:jar
# This creates target/dynatus.jar
# Install to local Maven repository (~/.m2) using Leiningen
lein install
# Or using deps.edn
clojure -X:install
Deployment is handled through GitHub Actions when you create a new release tag.
Set up GitHub secrets:
CLOJARS_USERNAME: Your Clojars usernameCLOJARS_TOKEN: Your Clojars deploy tokenCreate and push a version tag:
git tag v0.1.0-beta
git push origin v0.1.0-beta
GitHub Actions will automatically deploy to Clojars
If you need to deploy manually:
# Set environment variables
export CLOJARS_USERNAME=your-username
export CLOJARS_PASSWORD=your-deploy-token
# Deploy using Leiningen
lein deploy clojars
Once deployed to Clojars:
;; deps.edn
{:deps {org.clojars.morita/dynatus {:mvn/version "0.1.0-beta"}}}
;; Leiningen project.clj
[org.clojars.morita/dynatus "0.1.0-beta"]
The project uses GitHub Actions for automated testing and deployment:
ci.yml - Lightweight CI for every push/PR
test.yml - Comprehensive test suite
deploy_clojars.yml - Automated deployment
v0.1.0-beta)CLOJARS_USERNAME and CLOJARS_TOKEN secrets# Run all tests
make test
# Run tests with specific Java version
JAVA_HOME=/path/to/java11 make test
# Run with verbose output
clojure -M:test -m kaocha.runner --reporter documentation
# Run specific test namespace
clojure -M:test -m kaocha.runner --focus dynatus.core-test
git commit -am "Prepare release v0.1.0-beta"git tag v0.1.0-betagit push origin main --tagsYou can also trigger a deployment manually from GitHub Actions:
0.1.0-beta)For automated deployment, configure these secrets in your GitHub repository:
CLOJARS_USERNAME: Your Clojars usernameCLOJARS_TOKEN: Your Clojars deploy token (get from Clojars → Deploy Tokens)If you were using the previous migrate function, update your code:
;; Old API
(dynatus/migrate {:client client :path "resources/dynamo"})
;; New API
(dynatus/syncronizate {:client client :path "resources/dynamo"})
;; Returns more detailed information about the synchronization
The new API provides:
Copyright © 2025 MoritaHR
Distributed under the MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |