
AWS CDK Stack Dependencies
In this article, we will cover the different approaches for dealing with stack dependencies in your AWS CDK apps, with examples written in TypeScript.
Preface
✔️ We talk about having service references between stacks in code.
✔️ We talk through importing services between stacks using helpers.
✔️ We discuss the benefits and disadvantages of both approaches.

Introduction 👋🏽
When creating our AWS CDK application we typically split the application into one or more stacks as best practice, most of which will have a dependency on the other(s). There are several ways to ensure that we can pass the service dependencies between our stacks, and to deploy the stacks in the correct order.
In this short article, we will talk through the different methods of achieving this, what is happening under the hood in the CDK; whilst discussing the advantages and disadvantages of each approach.

Common Vocabulary 💬
Before we jump in, let’s discuss some common vocabulary that we will use:
✔️ Workloads — A workload is a set of components that together deliver business value (essentially a service or application). A workload is usually the level of detail that business and technology leaders communicate about. Examples of workloads are marketing websites, e-commerce websites, the back-ends for a mobile app, analytic platforms, etc.
✔️ Cloud Assembly — The Cloud Assembly is the output of the synthesis (build) operation. It is essentially a set of files, CloudFormation and directories (the cdk.out
folder), one of which is the manifest.json
file. It defines the set of instructions that are needed in order to deploy the assembly directory.
What are stack dependencies?👨💻
For me personally based on the patterns I use with the AWS CDK, we would typically have four stacks at most in a given workload, which once synthesised results in a cloud assembly (application as a deployable unit):
- Stateful: This is for our stateful resources such as databases.
- Stateless: This is for our more ephemeral services such as Lambda.
- Client (optional): This is to deploy a client application as part of our app (for example a React app served from an S3 bucket, CloudFront and Route53).
- Feature Flags (optional): This stack deploys the feature flags associated with our application, perhaps using App Config.
This is shown below as a dependency graph, keeping this simple with three stacks which make up our application in this particular workload:

“Consider keeping stateful resources (like databases) in a separate stack from stateless resources. You can then turn on termination protection on the stateful stack. This way, you can freely destroy or create multiple copies of the stateless stack without risk of data loss.
Stateful resources are more sensitive to construct renaming — renaming leads to resource replacement. Therefore, don’t nest stateful resources inside constructs that are likely to be moved around or renamed (unless the state can be rebuilt if lost, like a cache). This is another good reason to put stateful resources in their own stack.” — https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html
In this example above, we want to ensure that we deploy the stacks in the following order due to the dependencies between them:
Stateful > Stateless > Client.
This could be a very basic solution as shown below:

We can see that we have:
- Stateful: This stack consists of an Amazon DynamoDB table which will be referenced in the Stateless stack so the Lambda functions can use it.
- Stateless: This stack consists of our Amazon API Gateway which uses Lambda functions for the main business logic. This stack needs the DynamoDB table reference for Lambda permissions passed through environment variables and used in code to read and write to the table.
- Client: The client stack will need the Amazon API Gateway and Amazon CloudFront distribution deployed so it can be referenced in the client code.
Note: In a real-world example we would use Amazon Route53 with a deterministic URL sitting in front of the CloudFront distribution, for example,
api.something.com
orpr-123.something.com
, but we are keeping this super simple for the article to demonstrate hard dependencies.
This means the stateless stack depends on our stateful resources being provisioned like a DynamoDB table, and the client stack depends on the API Gateway being deployed in the stateless stack etc.
In the next section, we will look at two different approaches to adding our stack dependencies and ensuring that we deploy the stacks in the correct order.
👇 Before we go any further — please connect with me on LinkedIn for future blog posts and Serverless news https://www.linkedin.com/in/lee-james-gilmore/

What are we building? 👨💻

In this example, we will build the most basic of applications which allows us to create and store a new customer via our API; and will be split between two stacks: Stateful and Stateless. This is shown below:

We will split the approaches by folder in our example Github repo with folders approach-one
and approach-two
✔️ Approach One: Dependencies as references in code.
✔️ Approach Two: Dependencies imported between stacks using helpers.
What approaches can we use?
We can use two main approaches: dependencies defined between stacks in our AWS CDK app (references) where they are passed between them in stack props, or dependencies imported in our CDK code between stacks using helper methods where there are no hard references between them.
✔️ Dependencies as references
The agreed best practice from AWS when we have two stacks in the same AWS CDK app that need to share resources, is to pass a reference between them using the following approach shown below:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ApproachOneStatefulStack } from '../stateful/stateful';
import { ApproachOneStatelessStack } from '../stateless/stateless';
const app = new cdk.App();
const statefulStack = new ApproachOneStatefulStack(
app,
'ApproachOneStatefulStack',
{}
);
// we pass the table reference into the stateless stack
new ApproachOneStatelessStack(app, 'ApproachOneStatelessStack', {
table: statefulStack.table, // <-- reference is here
});
We can see above that we are passing the public property value ‘table
’ from the Stateful
stack and passing it into the arguments of the Stateless
stack through its props.
In the Stateful
stack the ‘table
’ public property is defined as so:
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
export class ApproachOneStatefulStack extends cdk.Stack {
public table: dynamodb.Table;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// we create the dynamodb table to store our customer records
this.table = new dynamodb.Table(this, 'Table', {
partitionKey: {
name: 'id',
type: dynamodb.AttributeType.STRING,
},
tableName: 'customer-table',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
}
}
In the Stateless
stack, we set the ‘table
’ argument using an interface for the props as shown below which extends the cdk.StackProps
:
export interface ApproachOneStatelessStackProps extends cdk.StackProps {
table: dynamodb.Table;
}
We can then use the reference as shown below:
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as path from 'path';
import { Construct } from 'constructs';
export interface ApproachOneStatelessStackProps extends cdk.StackProps {
table: dynamodb.Table;
}
export class ApproachOneStatelessStack extends cdk.Stack {
private table: dynamodb.Table;
constructor(
scope: Construct,
id: string,
props: ApproachOneStatelessStackProps
) {
super(scope, id, props);
const { table } = props;
this.table = table; // <--- reference via props
// create the lambda function which creates new customers
const createCustomerLambda: nodeLambda.NodejsFunction =
new nodeLambda.NodejsFunction(this, 'CreateCustomerLambda', {
functionName: 'create-customer-lambda',
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(
__dirname,
'src/adapters/primary/create-customer/create-customer.adapter.ts'
),
memorySize: 1024,
handler: 'handler',
tracing: lambda.Tracing.ACTIVE,
bundling: {
minify: true,
},
environment: {
...lambdaPowerToolsConfig,
// this is using the table reference below for the table name
TABLE_NAME: this.table.tableName,
},
});
// allow the lambda function to write to the table using the
// table's ARN reference under the hood
this.table.grantWriteData(createCustomerLambda);
...
}
}
The CDK will then automatically generate and manage CloudFormation outputs/imports under the hood for us and work out the stack deployment order for these dependencies. This is discussed below:
If the two stacks are in the same AWS CDK app, pass a reference between the two stacks. For example, save a reference to the resource’s construct as an attribute of the defining stack (
this.stack.uploadBucket = myBucket
). Then, pass that attribute to the constructor of the stack that needs the resource. — https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html#%23best-practices-apps-names
What this means is that our DynamoDB table becomes two exported outputs in the Stateful
stack automatically as shown below:

This means our Stateless
stack can now import them automatically where required under the hood in the AWS CDK.
If we look in the cdk.out
folder at our generated cloud assembly templates, and check out our Stateless
stack CloudFormation, we will see a direct import of the table name which is simply an import of the output ExportsOutputRefTableCD117FA1D18A8047
(i.e. the table name)
...
"Environment": {
"Variables": {
"LOG_LEVEL": "DEBUG",
"POWERTOOLS_LOGGER_LOG_EVENT": "true",
"POWERTOOLS_LOGGER_SAMPLE_RATE": "1",
"POWERTOOLS_TRACE_ENABLED": "enabled",
"POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS": "captureHTTPsRequests",
"POWERTOOLS_SERVICE_NAME": "customer-domain-service",
"POWERTOOLS_TRACER_CAPTURE_RESPONSE": "captureResult",
"POWERTOOLS_METRICS_NAMESPACE": "CustomerNamespace",
"TABLE_NAME": {
"Fn::ImportValue": "ApproachOneStatefulStack:ExportsOutputRefTableCD117FA1D18A8047"
},
"AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1"
}
},
...
This is the equivalent AWS CDK code of the following in our Stateless.ts
where we utilise the reference of the table
to access the tableName
property:
...
environment: {
...lambdaPowerToolsConfig,
TABLE_NAME: this.table.tableName,
},
...
The other exported output, ExportsOutputFnGetAttTableCD117FA1ArnE2C8C204
, refers to the ARN of the Amazon DynamoDB table, so we can import it using that approach when needed too in other stacks, with its value:
arn:aws:dynamodb:eu-west-1:12345678:table/ApproachOneStatefulStack-TableCD117FA1-J3ZZ6TUPCFBT
This exported value is used in the Stateless
stack when we use the following code:
...
// allow the lambda function to write to the table
this.table.grantWriteData(createCustomerLambda);
...
We can see from the generated raw CloudFormation in the cdk.out
folder as shown below how this is used using imports to get the Table ARN value:
...
"CreateCustomerLambdaServiceRoleDefaultPolicy43A3EE6F": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"dynamodb:BatchWriteItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Effect": "Allow",
"Resource": [
{
"Fn::ImportValue": "ApproachOneStatefulStack:ExportsOutputFnGetAttTableCD117FA1ArnE2C8C204"
},
{
"Ref": "AWS::NoValue"
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "CreateCustomerLambdaServiceRoleDefaultPolicy43A3EE6F",
},
"Metadata": {
"aws:cdk:path": "ApproachOneStatelessStack/CreateCustomerLambda/ServiceRole/DefaultPolicy/Resource"
}
},
...
Section Summary
This is typically the preferred approach when working with the AWS CDK, but we can also import the dependencies manually and force the stack deployment order as there are no hard links between them — let’s look in the next section at how we would do this.
✔️ Dependencies imported.
Another option is to specifically import resources between stacks in code using helper methods rather than relying on the AWS CDK to do this for us, and ensuring that we stipulate the correct stack deployment order manually.
We start by removing any references between our stacks, where our app.ts
now looks like this:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ApproachTwoStatefulStack } from '../stateful/stateful';
import { ApproachTwoStatelessStack } from '../stateless/stateless';
const app = new cdk.App();
// there are no references between our stacks
const statefulStack = new ApproachTwoStatefulStack(
app,
'ApproachTwoStatefulStack',
{}
);
const statelessStack = new ApproachTwoStatelessStack(
app,
'ApproachTwoStatelessStack',
{}
);
// we add the dependency on stacks here to guarantee ordering
statelessStack.addDependency(statefulStack);
stack.addDependency(stack)
(Python:stack.add_dependency(stack)
– Can be used to explicitly define dependency order between two stacks. This order is respected by the cdk deploy command when deploying multiple stacks at once. — https://docs.aws.amazon.com/cdk/v2/guide/stacks.html
You will also see from the code above that we now need to dictate what the stack deployment order is when deploying at the app level:
// we add the dependency on stacks here to guarantee ordering
statelessStack.addDependency(statefulStack);
In our Stateless
stack we change the Lambda function environment variable from a cross-stack reference to the table.tableName
, to a hard-coded value of ‘customer-table
’, as shown below:
environment: {
...lambdaPowerToolsConfig,
TABLE_NAME: 'customer-table',
},
This has now broken the reliance on a reference here between stacks.
We also change the permissions from a similar cross-stack reference, to the following using the static fromTableName
helper method where we import the DynamoDB table by name to get the reference to it:
// allow the lambda function to write to the table
const table = dynamodb.Table.fromTableName(this, 'Table', 'customer-table');
table.grantWriteData(createCustomerLambda);
If we look in our cdk.out
folder and look at our stateless stack we will see that we no longer rely on imports for the table permissions, and now rely on a CloudFormation Join (intrinsic function) which creates the reference for us in code:
"CreateCustomerLambdaServiceRoleDefaultPolicy43A3EE6F": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": {
{
"Action": [
"dynamodb:BatchWriteItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":dynamodb:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":table/customer-table"
]
]
},
{
"Ref": "AWS::NoValue"
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "CreateCustomerLambdaServiceRoleDefaultPolicy43A3EE6F",
"Roles": [
{
"Ref": "CreateCustomerLambdaServiceRole59B320F4"
}
]
},
"Metadata": {
"aws:cdk:path": "ApproachOneStatelessStack/CreateCustomerLambda/ServiceRole/DefaultPolicy/Resource"
}
},
Section Summary
This method also works fine and there are now no references between our stacks, but there are certainly advantages and disadvantages to both approaches in the next section.
Conclusion & Further Reading 📖
So to conclude; which approach should we use? Well, in reality, with larger apps we will probably have a combination of both methods as they grow and become more complex (especially where we can get cyclic dependencies to work around).
My go-to for sure is the approach of passing references which is also an AWS best practice, but there are also times when we need to use the static method approach on occasion.
One of the key gotchas to watch out for when using the cross-stack reference approach is breaking your app’s deployment with deployment deadlock, which is usually shown with the following error “Export cannot be deleted as it is in use by another Stack”.
The deadlock happens when you want to remove a reference from the consuming stack (Stateless
) when the stacks are already deployed, even when you never changed the producing stack (Stateful
) at all, but the CloudFormation validation kicks in and can get in a mess.
The reason for this is that the Stateful
stack tries to remove the exported Output in the CloudFormation template as it is no longer needed, but when deploying the template CloudFormation thinks there is already a reference in the Stateless
stack to it, and you get the following error:

The way we fix this is to deploy the Stateless
stack first so there are no longer any deployed cross-stack references in the CloudFormation service using the following command:
cdk deploy ApproachOneStatelessStack
We then fully deploy the app again using npm run deploy
The official fix is discussed below.
For more information on splitting out your AWS CDK stacks, check out the following four-part detailed series:
Wrapping up 👋🏽
I hope you enjoyed this short article, and if you did then please feel free to share and feedback!
Please go and subscribe to my YouTube channel for similar content!

I would love to connect with you also on any of the following:
https://www.linkedin.com/in/lee-james-gilmore/
https://twitter.com/LeeJamesGilmore
If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋
Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)
About me
“Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect, and Global Head of Technology & Architecture based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.
I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture, and technology.”
*** The information provided are my own personal views and I accept no responsibility for the use of the information. ***
You may also be interested in the following: