💸 Catch expensive AWS mistakes before deployment! See cost impact in GitHub PRs for Terraform & CDK. Join the Free Beta!
AWS CDK Project Structure: The Complete Guide to Organizing Your CDK App [2026]

AWS CDK Project Structure: The Complete Guide to Organizing Your CDK App [2026]

Learn how to structure AWS CDK projects from starter to enterprise scale. Covers constructs, stacks, testing, assets, and anti-patterns to avoid.

January 6th, 2026
21 min read
0 views
--- likes

As your AWS CDK application grows beyond a single stack, the initial structure created by cdk init quickly becomes insufficient. Without intentional organization, CDK projects become difficult to navigate, test, and maintain, leading to deployment risks and slower development velocity.

This guide shows you exactly how to structure CDK projects at every scale, from your first app to enterprise multi-team architectures. You'll learn the organizational patterns that AWS recommends, understand how construct levels affect your folder structure, and discover the anti-patterns that cause teams the most pain.

Whether you're starting fresh or restructuring an existing project, these patterns will help you build CDK applications that remain maintainable as your team and infrastructure grow.

Why CDK Project Structure Matters

Before diving into folder layouts, it's worth understanding why structure decisions have lasting consequences for your CDK projects.

Structure affects synthesis speed and deployment reliability. A poorly organized project with circular dependencies or overly large stacks takes longer to synthesize and creates deployment bottlenecks. When everything lives in one stack, a single change to a Lambda function forces redeployment of your entire infrastructure.

Poor structure creates deployment risks with stateful resources. Databases, S3 buckets, and VPCs have different lifecycles than the compute resources that use them. Mixing them carelessly can lead to accidental replacements during refactoring, with potential data loss.

Team scaling requires intentional organization. When multiple developers work on the same CDK application, clear boundaries between stacks and constructs prevent merge conflicts and enable parallel development. The folder structure becomes the communication mechanism for who owns what.

Well-Architected alignment improves governance. AWS CDK applications map directly to components as defined by the Well-Architected Framework. A CDK app codifies and delivers cloud application best practices, making your structure decisions visible to compliance and security reviews.

Refactoring becomes costly without upfront planning. While CDK now offers the cdk refactor command for safe restructuring, establishing good patterns from the start saves significant effort. Moving resources between stacks or renaming constructs still requires careful coordination in production environments.

The guiding principle from AWS is simple: start simple and add complexity only when requirements dictate. The CDK allows code refactoring to support new requirements without needing to architect for every scenario upfront.

Default CDK Project Structure Explained

When you install AWS CDK and run cdk init app --language typescript, the CLI creates a standardized directory structure that serves as the foundation for your project. Understanding each component helps you make informed decisions about how to extend it.

What cdk init Creates

A fresh TypeScript CDK project contains these files and folders:

my-cdk-app/
├── bin/
│   └── my-cdk-app.ts      # CDK app entry point
├── lib/
│   └── my-cdk-app-stack.ts # Stack definition
├── test/
│   └── my-cdk-app.test.ts  # Unit tests
├── cdk.json                # CDK CLI configuration
├── package.json            # Node.js dependencies
├── tsconfig.json           # TypeScript configuration
└── .npmignore              # Package publishing exclusions

The bin/ folder contains your application entry point. This file instantiates the CDK App and creates one or more stacks. A basic entry point looks like this:

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MyCdkAppStack } from '../lib/my-cdk-app-stack';

const app = new cdk.App();
new MyCdkAppStack(app, 'MyCdkAppStack');

The lib/ folder contains your stack definitions where AWS resources are defined using constructs. As your project grows, this folder expands to contain multiple stacks organized by lifecycle or domain.

The test/ folder mirrors your lib structure and contains unit tests. The CDK provides an assertions module that works with Jest to validate your synthesized CloudFormation templates.

Core Configuration Files (cdk.json, cdk.context.json)

Two configuration files control how CDK behaves, and understanding their difference prevents common confusion.

cdk.json is your primary configuration file. It tells the CDK CLI how to run your app and stores permanent configuration values:

{
  "app": "npx ts-node --prefer-ts-exts bin/my-cdk-app.ts",
  "context": {
    "@aws-cdk/core:newStyleStackSynthesis": true
  }
}

The context key in cdk.json holds values that should persist and not be cleared during development.

cdk.context.json stores cached context values retrieved from your AWS account during lookups. When you use Vpc.fromLookup() or query Availability Zones, the CDK caches the results here to ensure consistent synthesis across team members and CI/CD runs.

Context values can come from six sources, listed by precedence (highest to lowest):

  1. Automatically from the current AWS account
  2. Through --context option to the cdk command
  3. In cdk.context.json
  4. In the context key of cdk.json
  5. In ~/.cdk.json (global config)
  6. In the app using construct.node.setContext()

Important distinction: Only values in cdk.context.json can be reset with cdk context --reset or cdk context --clear. If you want to protect a context value from accidental clearing, move it to cdk.json.

Understanding Construct Levels (L1, L2, L3)

CDK constructs are categorized into three levels, each offering different degrees of abstraction. Understanding these levels is essential for organizing your lib folder effectively.

Level 1 (L1) Constructs are the lowest-level constructs that map directly to AWS CloudFormation resources. They are auto-generated from the CloudFormation resource specification and provide no abstraction. L1 constructs are named with a Cfn prefix followed by the CloudFormation resource identifier (e.g., CfnBucket for AWS::S3::Bucket).

Use L1 constructs when you need complete control over resource properties or when an L2 construct doesn't expose the configuration you need. New CloudFormation resources typically appear as L1 constructs within a week of CloudFormation support.

Level 2 (L2) Constructs are the most widely used construct type. They map to single CloudFormation resources but provide higher-level abstraction through intent-based APIs. L2 constructs include sensible default configurations, best practice security policies, and helper methods for defining properties and permissions.

For example, s3.Bucket represents an S3 bucket with encryption enabled by default and methods like grantRead() for managing permissions. L2 constructs ready for production are included in the AWS Construct Library.

Level 3 (L3) Constructs are the highest level of abstraction, also known as patterns. Each L3 construct can contain a collection of resources configured to work together to accomplish specific tasks. For example, ecsPatterns.ApplicationLoadBalancedFargateService creates a Fargate service fronted by an Application Load Balancer with sensible defaults for health checks, logging, and security groups.

When to Use Each Construct Level

The choice of construct level affects both your code organization and maintenance burden:

LevelWhen to UseTrade-offs
L1Need precise CloudFormation control, using brand-new AWS featuresVerbose, no built-in best practices
L2Standard resource creation with sensible defaultsMost common choice, good balance
L3Complete architectures matching common patternsLess flexible, opinionated choices

In practice, most production CDK code uses L2 constructs extensively, with L3 patterns for well-understood architectures and occasional L1 usage when you need to escape the abstraction.

Organizing Your CDK Source Code

Your source folder is where infrastructure lives, and its organization determines how easily your team can navigate and modify the codebase. AWS recommends modeling with constructs and deploying with stacks, which means building reusable constructs that get composed into stacks for specific deployment scenarios.

While cdk init creates a lib/ folder for stacks, production projects often use a src/ folder with subdirectories for stacks, constructs, aspects, and assets. This pattern provides better organization as projects grow.

Resource Grouping by Lifecycle

Resources with similar lifecycles should live together. A lifecycle represents how frequently resources change and what the impact of that change is. Databases change rarely and require careful migration planning. Lambda functions change frequently and can be deployed with minimal risk.

Here's an optimized project structure that groups resources by lifecycle, based on the AWS CDK Starter Kit:

my-cdk-app/
├── src/
│   ├── main.ts              # CDK app entry point
│   ├── aspects/             # Cross-cutting concerns
│   │   ├── index.ts
│   │   ├── permission-boundary-aspect.ts
│   │   ├── s3-aspect.ts
│   │   └── vpc-aspect.ts
│   ├── assets/              # Runtime code
│   │   ├── lambda/
│   │   │   └── example-function/
│   │   └── ecs/
│   │       └── example-container/
│   ├── bin/                 # Helper scripts
│   │   ├── env-helper.ts
│   │   ├── cicd-helper.ts
│   │   └── git-helper.ts
│   ├── constructs/          # Reusable components
│   │   ├── index.ts
│   │   ├── base-construct.ts
│   │   └── network-construct.ts
│   └── stacks/              # Stack definitions
│       ├── index.ts
│       ├── foundation-stack.ts
│       └── starter-stack.ts
├── test/
│   └── main.test.ts
├── .projenrc.ts             # Projen configuration
├── cdk.json
└── package.json

This structure differs from the default cdk init output in important ways:

  • src/main.ts serves as the app entry point, not bin/app.ts
  • src/stacks/ contains stack definitions instead of lib/
  • src/bin/ holds helper utilities (environment configuration, CI/CD helpers), not the app entry point
  • src/constructs/ organizes reusable components with a BaseConstruct that other constructs extend
  • src/aspects/ contains real, production-ready aspects for security and compliance

This organization helps you quickly identify the purpose of each directory and locate relevant components.

Separating Stateful and Stateless Resources

A fundamental decision in CDK project organization is whether to separate stateful resources (databases, S3 buckets, VPCs) from stateless resources (Lambda functions, ECS tasks, API Gateways) into distinct stacks.

Why separate them?

  1. Independent deployments: Updating a Lambda function shouldn't require touching your RDS instance. Separate stacks let you deploy stateless resources with high velocity while stateful resources remain stable.

  2. Easier rollback: If a deployment fails, rolling back a stateless stack is straightforward. With stateful resources mixed in, rollbacks become complex and risky.

  3. Clearer dependencies: When a database lives in its own stack, its consumers must explicitly reference it through cross-stack references. This makes dependencies visible and manageable.

  4. Improved fault tolerance: Issues in one stack are less likely to affect resources in another, isolating the blast radius of problems.

When to combine them:

There are scenarios where combining stateful and stateless resources makes sense. When resources share the same lifecycle and have no external dependencies, grouping them simplifies management. For example, a scheduled Fargate task with dedicated EFS storage might live in the same stack because the storage exists only for that task.

The key question is: do these resources deploy and change together? If yes, consider combining them. If the stateful resource serves multiple consumers or changes less frequently, separate them.

Custom Constructs Organization

The src/constructs/ folder holds reusable components that encapsulate organizational best practices. Custom constructs represent combinations of one or more AWS resources and can be imported across different stacks in your application.

When to create a custom construct:

  • You're repeating the same resource pattern across stacks
  • You want to enforce organizational standards (encryption, logging, tagging)
  • You need to combine multiple resources into a single logical unit

The BaseConstruct pattern:

A powerful approach is creating a BaseConstruct that other constructs extend. This provides common properties like environment, account, and region that all constructs can access:

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';

export interface BaseConstructProps {
  environment?: string;
}

export class BaseConstruct extends Construct {
  protected readonly environment: string;
  protected readonly account: string;
  protected readonly region: string;

  constructor(scope: Construct, id: string, props?: BaseConstructProps) {
    super(scope, id);
    this.environment = props?.environment || 'dev';
    this.account = cdk.Stack.of(this).account;
    this.region = cdk.Stack.of(this).region;
  }
}

Other constructs extend BaseConstruct to inherit these properties and apply environment-specific logic:

import { BaseConstruct } from './base-construct';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class NetworkConstruct extends BaseConstruct {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Environment-specific VPC configuration
    const cidrBlock = this.environment === 'production'
      ? '172.18.0.0/16'
      : '172.16.0.0/16';

    this.vpc = new ec2.Vpc(this, 'Vpc', {
      ipAddresses: ec2.IpAddresses.cidr(cidrBlock),
      natGateways: this.environment === 'production' ? 3 : 1,
    });
  }
}

This pattern centralizes environment detection and provides consistent access to account and region information across all your constructs.

For large organizations, consider publishing custom constructs to a private package repository like AWS CodeArtifact. This enables version management and sharing across multiple CDK applications.

CDK Aspects for Cross-Cutting Concerns

Aspects allow you to apply operations across all constructs in a scope before synthesis. They're the CDK mechanism for cross-cutting concerns like tagging, encryption enforcement, and security validation. The src/aspects/ folder in a well-organized project contains these reusable aspect implementations.

There are two types of aspects:

Read-only aspects scan the construct tree without making changes. Use them for validations (ensuring all S3 buckets have versioning enabled) or collecting information for audits.

Mutating aspects add new nodes or modify existing ones. Use them for adding tags, injecting default configurations, or automatically adding security groups.

Here's a practical S3 encryption aspect that both validates and auto-fixes:

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import type { IConstruct } from 'constructs';

export class BucketEncryptionAspect implements cdk.IAspect {
  visit(node: IConstruct): void {
    if (node instanceof s3.CfnBucket) {
      if (!node.bucketEncryption) {
        cdk.Annotations.of(node).addError(
          'S3 bucket must have server-side encryption configured'
        );
      }
    }
  }
}

export class BucketPublicAccessAspect implements cdk.IAspect {
  visit(node: IConstruct): void {
    if (node instanceof s3.CfnBucket) {
      // Auto-enable public access block
      node.publicAccessBlockConfiguration = {
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
      };
    }
  }
}

Apply aspects in your src/main.ts entry point:

import * as cdk from 'aws-cdk-lib';
import { BucketEncryptionAspect, BucketPublicAccessAspect } from './aspects';
import { StarterStack } from './stacks';

const app = new cdk.App();
new StarterStack(app, 'StarterStack');

// Apply aspects to all stacks
cdk.Aspects.of(app).add(new BucketEncryptionAspect());
cdk.Aspects.of(app).add(new BucketPublicAccessAspect());

app.synth();

Common Aspect Use Cases

Encryption enforcement: Verify and auto-configure that S3 buckets, EBS volumes, and RDS instances have encryption enabled.

Permission boundaries: Override IAM Role permission boundaries with your organization's policy for all roles created in the app.

VPC validation: Ensure VPC CIDRs fall within RFC1918 private ranges to prevent accidental public IP allocation.

Public access blocking: Automatically enable all S3 Public Access Block flags on every bucket.

Aspects can be applied at any level of the construct tree. Applying at the App level affects all stacks. Applying at the Stack level affects only constructs within that stack. Prefer using aspects to validate and set safe defaults rather than heavy runtime mutations that hide configuration intent.

Asset Management and Bundling

CDK supports bundling runtime code directly with your infrastructure definitions. The src/assets/ folder contains Lambda function code, Docker images, and other files that get deployed alongside your CloudFormation resources.

Lambda Function Code Placement

Organize Lambda code in the assets directory with subdirectories per function:

src/
├── assets/
│   ├── lambda/
│   │   └── example-lambda-function/
│   │       └── lambda_function.py
│   └── ecs/
│       └── example-container/
│           └── Dockerfile
└── stacks/
    └── compute-stack.ts

CDK bundles Lambda code during synthesis. For Python functions with dependencies, use bundling options:

new lambda.Function(this, 'ExampleFunction', {
  code: lambda.Code.fromAsset(path.join(__dirname, '../assets/lambda/example-lambda-function'), {
    bundling: {
      image: lambda.Runtime.PYTHON_3_12.bundlingImage,
      command: [
        'bash', '-c',
        'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output'
      ],
    },
  }),
  runtime: lambda.Runtime.PYTHON_3_12,
  handler: 'lambda_function.handler',
});

The asset path is mounted at /asset-input inside the Docker container, and the bundled output goes to /asset-output. CDK zips this output and uploads it during deployment.

AWS best practice is to combine infrastructure and runtime code in the same package. This enables testing both in isolation, versioning all code in sync, and evolving infrastructure and runtime together.

Container Image Assets

For containerized workloads, organize Dockerfiles in src/assets/ecs/:

src/
├── assets/
│   └── ecs/
│       └── example-container/
│           ├── Dockerfile
│           └── app/
│               └── index.js
└── stacks/
    └── ecs-stack.ts

CDK builds and publishes Docker images to ECR automatically. For ARM64 Lambda functions or ECS tasks, specify the architecture:

new lambda.DockerImageFunction(this, 'ArmFunction', {
  code: lambda.DockerImageCode.fromImageAsset(
    path.join(__dirname, '../assets/ecs/example-container')
  ),
  architecture: lambda.Architecture.ARM_64,
});

CDK passes --platform linux/arm64 to Docker for cross-platform builds.

Testing Structure and Organization

The test/ folder contains tests for your stacks and constructs. For simpler projects, a single test file works well. For larger projects, mirror your src/stacks/ structure:

my-cdk-app/
├── src/
│   ├── main.ts
│   ├── stacks/
│   │   ├── foundation-stack.ts
│   │   └── starter-stack.ts
│   └── constructs/
│       └── network-construct.ts
└── test/
    └── main.test.ts           # Tests for all stacks

Unit Tests vs Snapshot Tests

CDK supports two testing approaches with different purposes. For comprehensive testing strategies and patterns, see our CDK best practices guide.

Fine-grained assertions test specific aspects of generated CloudFormation templates. They're the primary testing method and support test-driven development:

import { Template } from 'aws-cdk-lib/assertions';

test('S3 bucket has encryption enabled', () => {
  const app = new cdk.App();
  const stack = new StorageStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  template.hasResourceProperties('AWS::S3::Bucket', {
    BucketEncryption: {
      ServerSideEncryptionConfiguration: [
        { ServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } }
      ]
    }
  });
});

Snapshot tests compare entire synthesized templates against stored baselines. They're useful when refactoring to ensure the output hasn't changed unexpectedly:

test('stack matches snapshot', () => {
  const app = new cdk.App();
  const stack = new StorageStack(app, 'TestStack');
  const template = Template.fromStack(stack);

  expect(template.toJSON()).toMatchSnapshot();
});

Use fine-grained assertions for regression detection and TDD. Use snapshot tests primarily during refactoring when you want to verify that changes don't alter the synthesized output.

For cross-stack testing, create the referenced resources in a separate stack first:

const app = new cdk.App();
const networkStack = new cdk.Stack(app, 'NetworkStack');
const vpc = new ec2.Vpc(networkStack, 'Vpc');

const computeStack = new ComputeStack(app, 'ComputeStack', { vpc });
const template = Template.fromStack(computeStack);

Project Structure Evolution: Starter to Enterprise

Project structure should evolve with your team and application complexity. Jumping to enterprise patterns on day one adds unnecessary overhead. Starting too simple creates technical debt when scaling.

Here's how to think about project structure as your needs grow:

Stage 1: Default cdk init Structure

For learning CDK or small proof-of-concepts, the default cdk init structure works:

my-app/
├── bin/
│   └── my-app.ts            # App entry point
├── lib/
│   └── my-app-stack.ts      # Stack definition
├── test/
│   └── my-app-stack.test.ts
└── cdk.json

This structure is appropriate when:

  • You're learning CDK
  • Building a quick prototype
  • One person owns the entire application
  • Resources have similar lifecycles

Stage 2: Production-Ready Structure

For production applications, adopt the src/-based structure with organized subdirectories. This is the structure used by the AWS CDK Starter Kit:

my-app/
├── src/
│   ├── main.ts              # App entry point
│   ├── aspects/             # Cross-cutting concerns
│   │   ├── permission-boundary-aspect.ts
│   │   └── s3-aspect.ts
│   ├── assets/              # Runtime code
│   │   ├── lambda/
│   │   └── ecs/
│   ├── bin/                 # Helper utilities
│   │   ├── env-helper.ts
│   │   └── cicd-helper.ts
│   ├── constructs/          # Reusable components
│   │   ├── base-construct.ts
│   │   └── network-construct.ts
│   └── stacks/              # Stack definitions
│       ├── foundation-stack.ts
│       └── starter-stack.ts
├── test/
│   └── main.test.ts
├── .projenrc.ts             # Projen configuration
└── cdk.json

This structure is appropriate when:

  • Building for production deployment
  • Multiple developers contributing
  • You need CI/CD integration
  • Environment-specific configurations required
  • Stateful resources need protection from frequent deployments

Stage 3: Enterprise with Shared Constructs

Large organizations benefit from separating shared constructs into their own packages:

organization-cdk/
├── packages/
│   ├── org-constructs/        # Published to CodeArtifact
│   │   ├── src/
│   │   │   ├── base-construct.ts
│   │   │   ├── compliant-bucket.ts
│   │   │   └── index.ts
│   │   └── package.json
│   └── app-one/
│       ├── src/
│       │   ├── main.ts
│       │   └── stacks/
│       └── package.json       # Depends on org-constructs
└── lerna.json                 # Or use Nx, Turborepo, pnpm workspaces

At this scale, consider:

  • Factory patterns for consistent resource creation across teams
  • Private package registry (CodeArtifact) for versioned construct libraries
  • Separate repositories for constructs used by multiple applications
  • Dedicated teams for maintaining organization-wide constructs

The key indicator for moving between stages is pain. If you're experiencing merge conflicts, slow deployments, or confusion about where code belongs, it's time to evolve your structure.

Managing Your CDK Project with Projen

Projen is the foundation of a well-maintained CDK project. Rather than manually configuring TypeScript, linting, testing, and CI/CD separately, Projen generates and manages all project configuration from a single .projenrc.ts file.

Why Projen for CDK Projects

Single source of truth. All project configuration lives in one TypeScript file. Need to update your Node.js version? Change it once in .projenrc.ts and Projen updates package.json, .nvmrc, GitHub workflows, and any other files that reference it. This is a CDK best practice for managing project configuration.

Synthesized configuration. Just like CDK synthesizes CloudFormation templates, Projen synthesizes project files. This means configuration files stay consistent and can be regenerated at any time with npx projen.

CDK-aware templates. Projen includes AwsCdkTypeScriptApp, a project type specifically designed for CDK applications. It understands CDK conventions and sets up the project accordingly.

Dependency management. Projen handles CDK version pinning, dev dependencies, and runtime dependencies. It can automatically configure Dependabot with smart grouping rules to keep dependencies updated without creating noise.

Integrated tooling. A single configuration enables Biome for linting/formatting, Jest for testing, TypeScript compilation, and GitHub workflows—all working together consistently.

What Projen Manages

Here's what a .projenrc.ts file controls in the AWS CDK Starter Kit:

import { awscdk } from 'projen';
import { NodePackageManager } from 'projen/lib/javascript';

const project = new awscdk.AwsCdkTypeScriptApp({
  name: 'my-cdk-app',
  cdkVersion: '2.221.0',           // CDK library version
  defaultReleaseBranch: 'main',
  packageManager: NodePackageManager.NPM,
  minNodeVersion: '22.18.0',       // Synced to .nvmrc and workflows

  // Dependencies
  deps: ['cloudstructs'],          // Runtime dependencies
  devDeps: ['@types/netmask'],     // Dev dependencies

  // Code quality
  biome: true,                     // Enables Biome linting/formatting

  // Automation
  dependabot: true,
  dependabotOptions: {
    scheduleInterval: DependabotScheduleInterval.WEEKLY,
  },
});

project.synth();

Running npx projen generates and keeps in sync:

  • package.json with all dependencies and scripts
  • tsconfig.json for TypeScript compilation
  • biome.jsonc for linting and formatting rules
  • .github/workflows/ for CI/CD pipelines
  • .nvmrc for Node.js version management
  • Jest configuration for testing
  • .gitignore with sensible defaults

Projen vs Manual Configuration

AspectManual ConfigurationProjen
Node.js versionUpdate in 5+ filesUpdate once in .projenrc.ts
CDK versionUpdate package.json, verify compatibilitySingle cdkVersion property
Linting rulesSeparate config files to maintainEmbedded in project definition
CI/CD workflowsHand-craft YAML filesGenerated from TypeScript
New team member setupFollow README instructionsRun npx projen
Drift detectionManual reviewnpx projen shows what changed

The key benefit is consistency. When you update your Projen configuration and run npx projen, all generated files update atomically. No more forgetting to update one of five places where a version is specified.

Get Started with a Production-Ready Template

Rather than building project structure and Projen configuration from scratch, you can jumpstart your CDK project with a production-ready template that implements all the best practices covered in this guide.

The AWS CDK Starter Kit provides everything you need for production CDK development:

Projen-Managed Configuration:

  • .projenrc.ts with CDK-optimized settings
  • Automated dependency updates via Dependabot
  • Biome for fast linting and formatting
  • Jest testing pre-configured
  • Environment-specific npm scripts generated automatically

Project Structure:

  • src/main.ts as app entry point with environment-aware configuration
  • src/stacks/ with FoundationStack (OIDC, IAM) and StarterStack templates
  • src/constructs/ with BaseConstruct and NetworkConstruct patterns
  • src/aspects/ with production-ready aspects (S3 encryption, VPC validation, permission boundaries)
  • src/assets/ organized for Lambda and ECS runtime code
  • src/bin/ with environment and CI/CD helper utilities

CI/CD & Security:

  • GitHub Actions workflows generated by Projen
  • OIDC-based AWS authentication (no stored secrets)
  • CDK diff comments on pull requests for change visibility
  • Branch-based deployment strategy for parallel development

For detailed documentation on the project structure and configuration options, see the Project Structure Guide.

Multi-Language Considerations

While the concepts in this guide apply across all CDK languages, folder conventions vary slightly:

LanguageSource FolderStack LocationTest Folder
TypeScript/JavaScriptsrc/ (recommended)src/stacks/test/
PythonProject root or src/*.py or src/stacks/tests/
Javasrc/main/java/Standard Maven structuresrc/test/java/
C#src/Standard .NET structuresrc/*/Tests/
GoProject root*.go*_test.go

The default cdk init creates lib/ and bin/ folders for TypeScript, but production projects benefit from the src/-based structure shown throughout this guide. Python projects typically use a flatter structure with stacks directly in the project root.

Regardless of language, the principles remain the same:

  • Group by lifecycle
  • Separate stateful from stateless
  • Create reusable constructs for repeated patterns
  • Keep tests alongside your source structure

Cross-Stack References and Dependencies

When resources in one stack need to reference resources in another stack, CDK automatically creates CloudFormation exports and imports. Understanding this mechanism helps you design stack boundaries effectively.

When you pass a VPC from one stack to another, CDK detects the cross-stack reference and:

  1. Creates an export in the producing stack
  2. Uses Fn::ImportValue in the consuming stack

This works seamlessly but has implications for stack deletion order and updates. You can't delete a stack while another stack imports its values.

Cross-region references require enabling the crossRegionReferences property:

const usEastStack = new Stack(app, 'UsEastStack', {
  env: { region: 'us-east-1' },
  crossRegionReferences: true,
});

const euWestStack = new Stack(app, 'EuWestStack', {
  env: { region: 'eu-west-1' },
  crossRegionReferences: true,
});

VPC cross-stack reference caution: For VPCs specifically, consider using Vpc.fromLookup() in the consuming stack instead of direct references. This avoids CloudFormation export/import constraints and provides more flexibility when stacks need independent deletion or updates.

For explicit dependency control beyond automatic detection, use the DependsOn relation to ensure resources deploy in the correct order.

Recent CDK Updates Affecting Structure

CDK continues to evolve with features that affect how you organize and refactor projects. Here are the most significant recent updates:

CDK Refactor (2025)

The cdk refactor command enables safe restructuring of CDK applications without resource replacement. Previously, renaming constructs or moving resources between stacks could cause CloudFormation to delete and recreate resources, potentially causing downtime and data loss for stateful resources.

Now you can:

  • Rename constructs without resource replacement
  • Move resources between stacks safely
  • Reorganize CDK applications without downtime

The command compares your current and new application states and uses CloudFormation's refactor capability to update resource mappings.

CLI and Library Version Split

As of February 2025, the CDK CLI and Construct Library have independent release cycles:

  • CDK CLI: Releases as 2.1000.0, 2.1001.0, etc.
  • Construct Library: Continues as 2.175.0, 2.176.0, etc.

The CLI repository has moved to github.com/aws/aws-cdk-cli. Keep your CLI at the latest 2.x version for compatibility with all Construct Library versions.

Node.js requirements: CDK now requires Node.js 20.x or 22.x. Support for Node.js 18.x ends November 30, 2025. Upgrade to Node.js 22.x for the longest compatibility runway.

Common Anti-Patterns to Avoid

Learning what not to do saves as much pain as knowing what to do. These anti-patterns consistently cause problems in CDK projects:

Environment variables inside constructs. Looking up environment variables within constructs creates hidden dependencies and makes testing difficult. For a complete list of anti-patterns and their solutions, see our CDK best practices guide. Accept all configuration through the props interface and limit environment variable lookups to the top-level app entry point:

// Anti-pattern
class MyConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    const env = process.env.ENVIRONMENT; // Don't do this
  }
}

// Correct pattern
class MyConstruct extends Construct {
  constructor(scope: Construct, id: string, props: MyConstructProps) {
    super(scope, id);
    const env = props.environment; // Accept via props
  }
}

Changing logical IDs of stateful resources. CloudFormation uses logical IDs to track resources. Changing a logical ID (by renaming a construct or moving it in the tree) causes resource replacement. For stateful resources like databases and S3 buckets, write unit tests that assert logical IDs remain stable.

Not testing infrastructure. Treat CDK code with the same rigor as application code. Tests catch misconfigurations before they reach production and document expected behavior for your team.

Relying solely on wrapper constructs for compliance. Custom constructs that enforce security policies are useful but insufficient. Developers can bypass them by using standard CDK constructs directly. Combine wrapper constructs with:

  • Service Control Policies at the organization level
  • Permission boundaries for IAM access
  • Aspects for validation across all constructs
  • CloudFormation Guard rules for additional checks

Circular dependencies between constructs. When Construct A depends on Construct B which depends on Construct A, synthesis fails. Design constructs with clear input/output interfaces and avoid tight coupling.

Conclusion

Project structure decisions compound over time. A well-organized CDK project today makes scaling, onboarding, and maintenance easier for years to come.

Key takeaways:

  1. Start simple and add complexity when needed. The default cdk init structure works for small projects. Evolve to multi-stack patterns as pain points emerge.

  2. Model with constructs, deploy with stacks. Build reusable constructs that encapsulate your patterns, then compose them into stacks for specific deployment scenarios.

  3. Separate stateful and stateless resources. Give databases, S3 buckets, and VPCs their own stacks to protect them from frequent deployment churn.

  4. Use aspects for cross-cutting concerns. Tagging, security validation, and compliance checks belong in aspects that apply uniformly across your construct tree.

  5. Structure for your team's scale. Single teams can work in single repositories. Multiple teams benefit from shared construct libraries published to package registries.

Start by evaluating your current project against the evolution patterns described here. Identify which stage you're at and whether your current pain points suggest it's time to evolve. The AWS CDK Starter Kit provides a production-ready starting point if you're building something new.

For deeper dives into specific topics, explore how CDK stacks work, learn about custom constructs, or understand cross-stack resource sharing. And if you want to consider whether CDK is the right choice for your situation, see how it compares to Terraform.

Build Scalable CDK Apps That Are Easy to Maintain

Transform your complex CDK codebase into a structured, reusable architecture. Get real-world expertise from someone who's built production CDK at scale.

Share this article on ↓

Subscribe to our Newsletter

Join ---- other subscribers!