DEV Community

Cover image for Do You Really Know the Difference Between L1, L2, and L3 CDK Constructs?
Yusuf Adeyemo for AWS Community Builders

Posted on • Originally published at blog.yusadolat.me on

Do You Really Know the Difference Between L1, L2, and L3 CDK Constructs?

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? πŸ€” 
Enter fullscreen mode Exit fullscreen mode

And then you see this error that makes you question everything:

Error: Cannot use property type 'BucketProps' with L1 construct 'CfnBucket' 
Enter fullscreen mode Exit fullscreen mode

"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.

A pyramid diagram showing three levels - L1 at the bottom (Basic LEGO bricks), L2 in the middle (Specialized LEGO pieces like wheels, windows), and L3 at the top (Complete LEGO sets like a castle or spaceship

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 } }); 
Enter fullscreen mode Exit fullscreen mode

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?

  1. Brand New AWS Services - When AWS releases a new service, L1 support comes first

  2. Debugging L2/L3 Issues - Sometimes you need to see what's really happening

  3. Migrating from CloudFormation - Direct 1:1 mapping makes migration easier

  4. 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) }); 
Enter fullscreen mode Exit fullscreen mode

See the difference? L2 constructs:

  • Use friendly property names (versioned vs versioningConfiguration)

  • Provide helper methods (grantRead())

  • Set security best practices by default

  • Handle resource dependencies automatically

Side-by-side comparison showing L1 code (20+ lines) vs L2 code (15 lines) creating the same bucket

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 
Enter fullscreen mode Exit fullscreen mode

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' } }] } }); 
Enter fullscreen mode Exit fullscreen mode

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 }); 
Enter fullscreen mode Exit fullscreen mode

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' } } } }]; 
Enter fullscreen mode Exit fullscreen mode

The Hidden Benefits of Each Level

L1 Benefits You Didn't Know About

  1. Immediate AWS Feature Support - No waiting for CDK updates

  2. CloudFormation Parity - Easy to convert existing templates

  3. Learning Tool - Understand what L2 constructs do under the hood

L2 Benefits That Save Time

  1. Automatic Security Defaults - Encryption enabled by default

  2. Cross-Service Integration - grant* methods handle IAM for you

  3. Type Safety - Catch errors at compile time, not deployment

L3 Benefits for Real Projects

  1. Proven Architectures - AWS Solutions Constructs follow best practices

  2. Compliance Ready - Many patterns are pre-validated for security

  3. 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'); 
Enter fullscreen mode Exit fullscreen mode

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

A comparison table with checkmarks showing when to use each construct level

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)