After you complete this article, you will have a solid understanding of:
What L1, L2, and L3 constructs actually are and when to use each
Why AWS created three different abstraction levels (and the hidden benefits)
How to avoid the most common CDK construct mistakes
When to break the rules and mix construct levels
Have You Ever Been Confused by CDK Construct Levels?
If you've ever started learning AWS CDK, you've probably encountered code like this and wondered why there are so many ways to create the same S3 bucket:
// Wait, what? Three different ways to create a bucket? import { CfnBucket } from 'aws-cdk-lib/aws-s3'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { StaticWebsite } from '@aws-solutions-constructs/aws-s3-cloudfront'; // Which one should I use? π€
And then you see this error that makes you question everything:
Error: Cannot use property type 'BucketProps' with L1 construct 'CfnBucket'
"But they're both S3 buckets! Why can't I use the same properties?"
Let me help you understand these construct levels once and for all.
What Are CDK Constructs Anyway?
Think of CDK constructs as LEGO blocks for your cloud infrastructure. Just like LEGO has basic bricks, specialized pieces, and complete sets, CDK has three levels of constructs.
Level 1 (L1) Constructs: The Raw CloudFormation Experience
L1 constructs are the most basic building blocks. They start with Cfn
(short for CloudFormation) and map directly to CloudFormation resources. No magic, no shortcuts.
import { CfnBucket } from 'aws-cdk-lib/aws-s3'; const bucket = new CfnBucket(this, 'MyL1Bucket', { bucketName: 'my-raw-bucket-2025', versioningConfiguration: { status: 'Enabled' }, publicAccessBlockConfiguration: { blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true } });
Notice how verbose this is? You have to configure EVERYTHING manually. It's like writing CloudFormation in TypeScript.
When Would You Ever Use L1 Constructs?
Brand New AWS Services - When AWS releases a new service, L1 support comes first
Debugging L2/L3 Issues - Sometimes you need to see what's really happening
Migrating from CloudFormation - Direct 1:1 mapping makes migration easier
Edge Cases - When you need a specific CloudFormation property not exposed in L2
Level 2 (L2) Constructs: The Sweet Spot
L2 constructs are what most developers use daily. They provide sensible defaults, helper methods, and hide complexity while still giving you control.
import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; const bucket = new Bucket(this, 'MyL2Bucket', { bucketName: 'my-friendly-bucket-2025', versioned: true, encryption: BucketEncryption.S3_MANAGED, removalPolicy: RemovalPolicy.DESTROY // Much cleaner! }); // Look at these helper methods! bucket.grantRead(myLambdaFunction); bucket.addLifecycleRule({ expiration: Duration.days(90) });
See the difference? L2 constructs:
Use friendly property names (
versioned
vsversioningConfiguration
)Provide helper methods (
grantRead()
)Set security best practices by default
Handle resource dependencies automatically
Level 3 (L3) Constructs: Complete Solutions
L3 constructs (also called patterns) are pre-built architectures for common use cases. They combine multiple resources into a working solution.
import { StaticWebsite } from '@aws-solutions-constructs/aws-s3-cloudfront'; const website = new StaticWebsite(this, 'MyWebsite', { websiteIndexDocument: 'index.html', websiteErrorDocument: 'error.html' }); // That's it! You just created: // - S3 bucket with proper website configuration // - CloudFront distribution // - Origin Access Identity // - Proper IAM policies // - HTTPS redirect // - Security headers
With just a few lines, you get a production-ready static website setup that would take hundreds of lines in L1.
Common Mistakes That Will Drive You Crazy
Mistake #1: Mixing Property Types
// π« This won't work! const bucket = new CfnBucket(this, 'MyBucket', { encryption: BucketEncryption.S3_MANAGED // L2 property type }); // β
Use the correct L1 property type const bucket = new CfnBucket(this, 'MyBucket', { bucketEncryption: { serverSideEncryptionConfiguration: [{ serverSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }] } });
Mistake #2: Assuming L3 Constructs Are Always Better
// Using L3 when you need specific customization const website = new StaticWebsite(this, 'MyWebsite', { // Oh no! I can't set specific CloudFront behaviors // or custom cache policies here! π± }); // Sometimes L2 gives you more control const bucket = new Bucket(this, 'WebBucket'); const distribution = new CloudFrontWebDistribution(this, 'MyDist', { // Full control over every setting });
Mistake #3: Not Using Escape Hatches
What if you need to modify an L2 construct's underlying L1 resource?
const bucket = new Bucket(this, 'MyBucket'); // Access the L1 construct (escape hatch) const cfnBucket = bucket.node.defaultChild as CfnBucket; // Now you can set ANY CloudFormation property cfnBucket.analyticsConfigurations = [{ id: 'my-analytics', storageClassAnalysis: { dataExport: { destination: { bucketArn: 'arn:aws:s3:::my-analytics-bucket' } } } }];
The Hidden Benefits of Each Level
L1 Benefits You Didn't Know About
Immediate AWS Feature Support - No waiting for CDK updates
CloudFormation Parity - Easy to convert existing templates
Learning Tool - Understand what L2 constructs do under the hood
L2 Benefits That Save Time
Automatic Security Defaults - Encryption enabled by default
Cross-Service Integration -
grant*
methods handle IAM for youType Safety - Catch errors at compile time, not deployment
L3 Benefits for Real Projects
Proven Architectures - AWS Solutions Constructs follow best practices
Compliance Ready - Many patterns are pre-validated for security
Rapid Prototyping - Get a working system in minutes
Creating Your Own L3 Construct
Here's a practical example of creating your own pattern:
import { Construct } from 'constructs'; import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda'; import * as path from 'path'; export class SecureDataProcessor extends Construct { public readonly bucket: Bucket; public readonly processor: Function; constructor(scope: Construct, id: string) { super(scope, id); // Create encrypted bucket this.bucket = new Bucket(this, 'DataBucket', { encryption: BucketEncryption.KMS_MANAGED, versioned: true, enforceSSL: true }); // Create processing Lambda this.processor = new Function(this, 'Processor', { runtime: Runtime.NODEJS_18_X, handler: 'index.handler', code: Code.fromAsset(path.join(__dirname, 'lambda')) }); // Wire them together this.bucket.grantRead(this.processor); this.bucket.addEventNotification( EventType.OBJECT_CREATED, new LambdaDestination(this.processor) ); } } // Now anyone can use your pattern! const dataProcessor = new SecureDataProcessor(this, 'MyProcessor');
When to Use Each Construct Level
Use L1 when:
You need bleeding-edge AWS features
Migrating from CloudFormation
Debugging CDK issues
You need a specific CloudFormation property
Use L2 when:
Building most production applications
You want security best practices by default
You need to integrate multiple services
You value developer productivity
Use L3 when:
Implementing common patterns
Rapid prototyping
Enforcing organizational standards
You don't need heavy customization
The Future of CDK Constructs
AWS is continuously improving CDK constructs. New services get L1 support immediately through CloudFormation, L2 constructs follow within weeks or months, and the community creates L3 patterns for common use cases.
Remember: There's no "wrong" construct level. Each serves a purpose, and experienced CDK developers often mix levels within the same application.
Was this article helpful for you? If so, kindly follow on twitter @yusadolat
]]>
Top comments (0)