If you're building infrastructure with the AWS CDK, understanding constructs is fundamental to everything you'll do. Constructs are the building blocks that let you define AWS resources using familiar programming languages instead of JSON or YAML templates.
In this guide, you'll learn what constructs are, how the three construct levels (L1, L2, L3) work, and when to use each one. By the end, you'll understand how constructs form a hierarchy, how to create your own custom constructs, and the best practices that separate well-designed CDK applications from tangled messes.
What is an AWS CDK Construct?
A construct is a component within your CDK application that represents one or more AWS CloudFormation resources and their configuration. Think of constructs as cloud components that encapsulate everything CloudFormation needs to create your infrastructure.
Every construct you work with is a class that extends the base Construct class from the constructs package. This might seem like a small detail, but it's actually important because this base class is the foundation of the entire CDK ecosystem.
The Building Blocks of Cloud Infrastructure
When you define infrastructure with the CDK, you're essentially composing constructs together like building blocks. A simple construct might represent a single S3 bucket. A more complex construct could represent an entire application architecture with load balancers, containers, databases, and the networking to connect them.
The real power comes from reusability. Once you create a construct, you can instantiate it multiple times across different CDK stacks, share it with your team, or even publish it for the community to use. Updates to your construct library automatically propagate to every application using it, just like any other code library.
How Constructs Relate to CloudFormation
Under the hood, every CDK application synthesizes into a CloudFormation template. When you run cdk synth, the CDK traverses your construct tree and generates the corresponding CloudFormation resources. This means constructs are ultimately an abstraction layer on top of CloudFormation, but one that gives you the full power of programming languages.
This relationship means you get the reliability of CloudFormation (state management, rollbacks, drift detection) combined with the flexibility of real programming languages (loops, conditionals, abstractions, type checking).
The Construct Programming Model (CPM)
Here's something that often surprises developers: the construct pattern isn't exclusive to AWS. The Construct Programming Model (CPM) makes constructs portable across different infrastructure tools:
- CDK for Terraform (CDKtf): Use constructs to generate Terraform configurations
- CDK for Kubernetes (CDK8s): Define Kubernetes manifests with constructs
- Projen: Build project configurations using the same patterns
This means the skills you develop working with AWS CDK constructs transfer directly to other infrastructure-as-code contexts.
The Three Construct Levels: L1, L2, and L3
Constructs from the AWS Construct Library fall into three categories, each offering a different level of abstraction. Understanding these levels is essential for choosing the right tool for each job.
The higher the level, the easier to configure (requiring less expertise). The lower the level, the more customization available (requiring more expertise). Here's how they compare:
L1 Constructs (CFN Resources)
L1 constructs are the lowest-level constructs, providing no abstraction at all. Each L1 construct maps directly to a single CloudFormation resource type.
You can identify L1 constructs by their Cfn prefix. For example, CfnBucket corresponds to the AWS::S3::Bucket CloudFormation resource. These constructs are automatically generated from the CloudFormation resource specification, which is why new CloudFormation resources typically appear as L1 constructs within one week of release.
When to use L1 constructs:
- You need access to properties not yet exposed by L2 constructs
- You're migrating existing CloudFormation templates to CDK
- You require precise control over every resource property
- A service doesn't have an L2 construct available yet
import * as s3 from 'aws-cdk-lib/aws-s3';
// L1 construct - direct CloudFormation mapping
new s3.CfnBucket(this, 'MyL1Bucket', {
bucketName: 'my-bucket-name',
versioningConfiguration: {
status: 'Enabled'
},
bucketEncryption: {
serverSideEncryptionConfiguration: [{
serverSideEncryptionByDefault: {
sseAlgorithm: 'AES256'
}
}]
}
});
The trade-off is clear: L1 constructs give you complete control, but you're responsible for configuring everything correctly, including security settings that L2 constructs handle automatically.
L2 Constructs (Curated Constructs)
L2 constructs are the workhorses of CDK development. They're thoughtfully designed by the CDK team to provide a higher-level abstraction while still mapping to single CloudFormation resources.
The key advantages of L2 constructs:
- Sensible defaults: Security best practices are built in
- Grant methods: Easily manage IAM permissions with
grantRead(),grantWrite(), etc. - Boilerplate generation: Glue logic and configuration handled automatically
- Intent-based API: Focus on what you want, not how to configure it
import * as s3 from 'aws-cdk-lib/aws-s3';
// L2 construct - curated abstraction with sensible defaults
new s3.Bucket(this, 'MyL2Bucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // Security by default
enforceSSL: true
});
Notice how the L2 version is cleaner and automatically includes security features like blockPublicAccess. The CDK team has already made the secure choice the default choice.
For a practical example, see how to create an Amazon S3 bucket with AWS CDK using L2 constructs with proper encryption and access controls.
L3 Constructs (Patterns)
L3 constructs, also called patterns, represent the highest level of abstraction. Each L3 construct contains a collection of resources configured to work together, enabling you to deploy entire architectures with minimal configuration.
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
// L3 construct - complete architecture pattern
new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'MyService', {
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample')
},
publicLoadBalancer: true
});
This single construct creates:
- A Fargate service
- An Application Load Balancer
- Target groups and listeners
- Security groups with appropriate rules
- IAM roles and policies
- CloudWatch log groups
You can explore this pattern in detail with the Application Load Balanced Fargate Service example or learn about scheduled workloads with the Scheduled Fargate Task guide.
When to Use L1 vs L2 vs L3: A Decision Framework
Choosing the right construct level depends on your specific needs. Use this decision framework:
Start with L2 constructs for most resources. They provide the best balance of convenience and flexibility. Drop down to L1 when you need properties not yet exposed, or rise to L3 when you need a complete architectural pattern.
Understanding Scope, ID, and Props
Every construct takes three parameters when initialized. Understanding these parameters is essential for working effectively with the CDK.
Scope: Your Construct's Place in the Tree
The scope parameter determines where your construct sits in the construct tree. It's the parent or owner of the construct you're creating.
In practice, you almost always pass this (or self in Python) as the scope, representing the current construct where you're defining the new one.
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 'this' is the scope - this bucket belongs to MyStack
new s3.Bucket(this, 'DataBucket');
}
}
The scope creates a parent-child relationship that affects how resources are named, how dependencies are tracked, and how the construct tree is traversed during synthesis.
ID: The Unique Identifier
The ID is a string that must be unique within its scope. It serves as a namespace for everything defined within the construct and is used to generate:
- CloudFormation logical IDs
- Resource physical names (when not explicitly set)
- Construct tree paths
Important: Changing a construct's ID changes its CloudFormation logical ID, which CloudFormation interprets as deleting the old resource and creating a new one. This can cause unexpected data loss for stateful resources like databases or S3 buckets with data.
// These two buckets can have the same ID because they're in different scopes
new s3.Bucket(stackA, 'SharedBucket');
new s3.Bucket(stackB, 'SharedBucket'); // OK - different scope
IDs only need to be unique within their scope, allowing you to reuse the same ID across different parent constructs.
Props: Configuration Made Simple
Props are the configuration options for your construct, passed as a single object (TypeScript/JavaScript) or keyword arguments (Python). Each construct type defines its own props interface.
The beauty of L2 constructs is that most props are optional with sensible defaults:
// Minimal - uses all defaults
new s3.Bucket(this, 'SimpleBucket');
// Configured - override specific defaults
new s3.Bucket(this, 'ConfiguredBucket', {
versioned: true,
lifecycleRules: [{
expiration: cdk.Duration.days(90)
}]
});
Different languages handle props differently:
| Language | Props Pattern |
|---|---|
| TypeScript/JavaScript | Object with interface (e.g., BucketProps) |
| Python | Keyword arguments with snake_case naming |
| Java | Builder pattern with method chaining |
| C# | Props object with inner classes |
| Go | Struct with pointer syntax |
The Construct Tree Hierarchy
Constructs form a hierarchy known as the construct tree. Understanding this structure helps you design maintainable CDK applications and troubleshoot issues when they arise.
App, Stack, and Construct Relationship
The construct tree has a specific structure:
- App: The root of the construct tree, representing your entire CDK application
- Stack: The unit of deployment, mapping to a CloudFormation stack
- Constructs: The resources and components within stacks
All constructs representing AWS resources must be defined within a Stack, either directly or as children of other constructs within that stack. The App and Stack constructs are special - they provide context rather than directly configuring AWS resources.
You can learn more about how stacks work in the AWS CDK Stack guide, and explore patterns for sharing resources across stacks when your architecture spans multiple deployment units.
Logical IDs and Physical Names
The CDK generates CloudFormation logical IDs based on the construct's ID and its path in the tree. Understanding this helps you predict resource behavior:
- Logical ID: Generated from construct path + hash (e.g.,
ProductionVpcConstruct8F42B11) - Physical Name: The actual name visible in AWS Console (e.g.,
production-vpc-8f42b11-xyz)
Every construct has a node attribute that provides access to the tree:
const bucket = new s3.Bucket(this, 'MyBucket');
console.log(bucket.node.id); // 'MyBucket'
console.log(bucket.node.path); // 'MyStack/MyBucket'
console.log(bucket.node.scope); // Reference to parent (the stack)
Warning: Moving a construct or changing its ID changes the logical ID, causing CloudFormation to replace the resource. For stateful resources, this means data loss unless you handle the migration carefully.
How to Create a Custom Construct
Creating custom constructs is where the CDK truly shines. You can encapsulate complex configurations, enforce organizational standards, and share reusable components.
Basic Construct Structure
Every custom construct follows the same pattern: extend the Construct base class and call super(scope, id) in the constructor.
import { Construct } from 'constructs';
export interface MyConstructProps {
// Define your construct's configuration options
}
export class MyConstruct extends Construct {
constructor(scope: Construct, id: string, props?: MyConstructProps) {
super(scope, id);
// Define resources and configuration here
}
}
Before you start building constructs, make sure you have the CDK installed. Check out the guide on how to install AWS CDK if you haven't already set up your environment. For organizing your custom constructs in production projects, see our CDK project structure guide.
Adding Properties with an Interface
Define a props interface to expose configuration options for your construct:
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
export interface NotifyingBucketProps {
/**
* The SNS topic to notify when objects are created
* @default - A new topic is created
*/
readonly topic?: sns.ITopic;
/**
* The S3 event types that trigger notifications
* @default - s3.EventType.OBJECT_CREATED
*/
readonly eventTypes?: s3.EventType[];
}
export class NotifyingBucket extends Construct {
public readonly bucket: s3.Bucket;
public readonly topic: sns.Topic;
constructor(scope: Construct, id: string, props: NotifyingBucketProps = {}) {
super(scope, id);
this.bucket = new s3.Bucket(this, 'Bucket');
this.topic = props.topic ?? new sns.Topic(this, 'Topic');
const eventTypes = props.eventTypes ?? [s3.EventType.OBJECT_CREATED];
for (const eventType of eventTypes) {
this.bucket.addEventNotification(eventType, new s3n.SnsDestination(this.topic));
}
}
}
This pattern demonstrates several best practices:
- Props interface with JSDoc documentation
- Default values using nullish coalescing (
??) - Exposing created resources as public properties for reference
Composition Over Inheritance
The CDK strongly favors composition over inheritance. Custom constructs should extend the base Construct class and compose other constructs within, rather than extending specific resource classes.
Do this:
// Composition - preferred pattern
export class SecureBucket extends Construct {
public readonly bucket: s3.Bucket;
constructor(scope: Construct, id: string) {
super(scope, id);
this.bucket = new s3.Bucket(this, 'Bucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true
});
}
}
Avoid this:
// Inheritance - not recommended
export class SecureBucket extends s3.Bucket {
constructor(scope: Construct, id: string, props?: s3.BucketProps) {
super(scope, id, {
encryption: s3.BucketEncryption.S3_MANAGED,
...props // User could override security settings
});
}
}
Composition gives you full control over what's configurable and ensures your security defaults can't be accidentally overridden.
Complete Custom Construct Example
Here's a practical example of a construct that creates a Lambda function with associated resources:
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
export interface ProcessorFunctionProps {
readonly handler: string;
readonly code: lambda.Code;
readonly environment?: Record<string, string>;
readonly timeout?: cdk.Duration;
readonly memorySize?: number;
}
export class ProcessorFunction extends Construct {
public readonly function: lambda.Function;
public readonly logGroup: logs.LogGroup;
constructor(scope: Construct, id: string, props: ProcessorFunctionProps) {
super(scope, id);
// Create log group with retention
this.logGroup = new logs.LogGroup(this, 'LogGroup', {
retention: logs.RetentionDays.TWO_WEEKS,
removalPolicy: cdk.RemovalPolicy.DESTROY
});
// Create Lambda function
this.function = new lambda.Function(this, 'Function', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: props.handler,
code: props.code,
timeout: props.timeout ?? cdk.Duration.seconds(30),
memorySize: props.memorySize ?? 256,
environment: props.environment,
logGroup: this.logGroup
});
}
/**
* Grant read permissions to another construct
*/
public grantInvoke(grantee: iam.IGrantable): iam.Grant {
return this.function.grantInvoke(grantee);
}
}
This example shows how to expose helper methods (like grantInvoke) that proxy to internal resources, making your construct feel native to the CDK ecosystem.
For more complex scenarios involving IAM roles, see how to assign a custom role to a Lambda function.
Working with Constructs: Methods and Attributes
After instantiating a construct, you interact with it through methods and properties. The AWS Construct Library follows consistent patterns that make this intuitive.
Grant Methods for IAM Permissions
Most L2 constructs provide grant methods that automatically create the correct IAM permissions. This is one of the biggest productivity gains from using L2 over L1 constructs.
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const bucket = new s3.Bucket(this, 'DataBucket');
const processor = new lambda.Function(this, 'Processor', { /* ... */ });
// Grant read permissions - CDK generates the correct IAM policy
bucket.grantRead(processor);
// If the bucket uses KMS encryption, CDK automatically grants
// the necessary KMS decrypt permissions too!
Common grant methods include:
grantRead()- Read-only accessgrantWrite()- Write accessgrantReadWrite()- Full accessgrantPut(),grantDelete()- Specific operation access- Service-specific methods like
grantSendMessages()for SQS
The grant methods implement least-privilege permissions based on intent, saving you from manually crafting IAM policies.
Resource Attributes and Cross-References
Constructs expose attributes that you can reference from other constructs. This is how you connect resources together:
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const queue = new sqs.Queue(this, 'JobQueue');
const processor = new lambda.Function(this, 'Processor', {
// ... other config
environment: {
QUEUE_URL: queue.queueUrl, // Reference the queue URL
QUEUE_ARN: queue.queueArn // Reference the ARN
}
});
queue.grantSendMessages(processor);
Common attributes include:
- ARNs:
bucket.bucketArn,function.functionArn - Names:
bucket.bucketName,table.tableName - URLs:
queue.queueUrl,api.url - Service-specific:
distribution.distributionDomainName
When you reference an attribute from a construct in a different stack, CDK automatically creates the CloudFormation exports and imports needed for cross-stack references. Learn more about this in how to share resources across stacks.
The Construct Ecosystem
Beyond the constructs you create yourself, there's a rich ecosystem of pre-built constructs available.
AWS Construct Library
The AWS Construct Library is included in the aws-cdk-lib package and contains constructs developed and maintained by AWS. It's organized into modules for each AWS service:
aws-cdk-lib/aws-s3- S3 buckets and related resourcesaws-cdk-lib/aws-lambda- Lambda functions, layers, and event sourcesaws-cdk-lib/aws-ecs- ECS clusters, services, and tasksaws-cdk-lib/aws-ec2- VPCs, subnets, security groups
This library is your primary source for L2 and L3 constructs. The AWS team continuously adds new constructs and improves existing ones.
Construct Hub: Discover and Share
Construct Hub is an online registry where you can discover constructs from AWS, third parties, and the open-source community. You can browse by:
- CDK version compatibility
- Download statistics
- AWS service categories
- Publisher verification status
It's also where you can publish your own constructs to share with the community.
Third-Party Constructs: Cautions and Best Practices
The open nature of the construct ecosystem means anyone can publish constructs. While this enables innovation, it also requires caution:
Before using third-party constructs:
- Review the source code - Understand what resources are created
- Verify the publisher - Check if they're a trusted source
- Pin exact versions - Prevent supply chain attacks from malicious updates
- Test in isolation - Deploy to sandbox environments first
- Check maintenance status - Abandoned constructs become security liabilities
AWS Solutions Constructs is a notable third-party library that provides vetted, well-architected patterns combining multiple AWS services. These are built using AWS best practices and reviewed by AWS architects.
Best Practices for Constructs
Following these practices will help you build maintainable, scalable CDK applications. For production-ready patterns including testing, security, and CI/CD, see our complete CDK best practices guide.
Model with Constructs, Deploy with Stacks
This is the most important principle for CDK architecture:
- Constructs represent logical units of your application (API, database, processing pipeline)
- Stacks represent deployment boundaries (what gets deployed together)
Don't create one stack per resource or one massive stack for everything. Instead, compose your application from constructs, then organize those constructs into stacks based on deployment requirements.
// Good: Logical units as constructs
class WebsiteConstruct extends Construct {
// S3, CloudFront, Route53 - all related to the website
}
class ApiConstruct extends Construct {
// API Gateway, Lambda, DynamoDB - all related to the API
}
// Stacks for deployment organization
class ProductionStack extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
new WebsiteConstruct(this, 'Website');
new ApiConstruct(this, 'Api');
}
}
For guidance on organizing larger projects, see how to optimize your AWS CDK project structure.
If you want to see these patterns in action with production-ready defaults, check out the AWS CDK Starter Kit. It demonstrates how to organize constructs, structure stacks for multi-environment deployments, and includes secure CI/CD with GitHub Actions.
Infrastructure and Runtime Code Together
The CDK bundles runtime assets (Lambda code, Docker images) alongside infrastructure definitions. Use this to your advantage by keeping infrastructure and runtime code in the same constructs:
export class DataProcessor extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// Infrastructure and runtime code together
new lambda.Function(this, 'Processor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/processor'), // Runtime code
});
}
}
This approach enables:
- Testing infrastructure and logic together
- Versioning both in sync
- Sharing complete functionality as a single unit
- Easier reasoning about what a construct does
Escape Hatches: When L2 Isn't Enough
Sometimes L2 constructs don't expose the property you need, or a new CloudFormation feature isn't available yet. The CDK provides escape hatches for these situations.
Accessing the L1 Construct
Every L2 construct provides access to its underlying L1 construct through a property like node.defaultChild:
import * as s3 from 'aws-cdk-lib/aws-s3';
const bucket = new s3.Bucket(this, 'MyBucket');
// Access the underlying L1 CfnBucket
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;
// Modify properties not exposed by L2
cfnBucket.addPropertyOverride('AnalyticsConfigurations', [{
Id: 'EntireBucket',
StorageClassAnalysis: {
DataExport: {
Destination: {
BucketArn: analyticsBucket.bucketArn,
Format: 'CSV'
}
}
}
}]);
Going Back Up: Un-Escape Hatches
You can also convert an L1 construct to an L2 construct using static methods like .fromCfnXxxxx():
import * as s3 from 'aws-cdk-lib/aws-s3';
// Start with L1 for full control
const cfnBucket = new s3.CfnBucket(this, 'MyBucket', {
bucketName: 'my-specific-bucket-name'
});
// Convert to L2 to use convenience methods
const bucket = s3.Bucket.fromCfnBucket(cfnBucket);
// Now you can use grant methods
bucket.grantRead(myLambdaFunction);
This is useful when you need L1 control for some properties but still want access to L2 convenience methods.
Frequently Asked Questions
What is the difference between a construct and a stack?
When should I use L1 vs L2 vs L3 constructs?
Can I mix different construct levels in the same stack?
How do I create a reusable construct?
What happens if I change a construct's ID?
How do constructs relate to CloudFormation?
Wrapping Up
Constructs are the foundation of everything you build with the AWS CDK. Understanding the three levels (L1, L2, L3), how they form a hierarchy, and how to create your own opens up powerful patterns for infrastructure development.
Start with L2 constructs for most work, use L3 patterns for common architectures, and drop to L1 only when you need complete control. As your CDK applications grow, invest in creating custom constructs that encapsulate your organization's patterns and standards.
The skills you develop working with constructs will serve you well, whether you're building AWS infrastructure, exploring CDK for Terraform, or contributing to the open-source construct ecosystem.
What patterns are you building with constructs? I'd love to hear about your CDK architecture in the comments below.
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.
![What is an AWS CDK Construct? Complete Guide to L1, L2, and L3 [2026]](/_next/image?url=%2Fimages%2Fblog%2Faws-cdk-construct%2Faws-cdk-construct.webp&w=3840&q=70)


