AWS CDK Code Organization: Service-Based vs Domain-Based Architecture Patterns
Learn when to use service-based, domain-based, feature-based, or layer-based organization patterns in AWS CDK projects. Includes decision frameworks, working examples, and migration strategies for maintainable infrastructure code.
Abstract
AWS CDK projects often start with unclear organization strategies, leading to tight coupling, circular dependencies, and deployment bottlenecks as they scale. This guide examines five organization patterns - service-based, domain-based, feature-based, layer-based, and hybrid - with working TypeScript examples and decision frameworks to help teams structure CDK projects that align with their business needs, team structure, and deployment requirements.
Problem Context
Working with CDK projects across different teams, I've noticed a recurring pattern: projects start with good intentions but gradually become difficult to maintain. The infrastructure code grows organically, files are placed wherever convenient, and before long, teams struggle with merge conflicts, unclear ownership, and deployment dependencies they can't untangle.
The technical challenges manifest in several ways. Teams organizing by AWS service (creating separate folders for lambda/, dynamodb/, api-gateway/) find that changes to a single business feature require touching multiple directories. Cross-stack references create deployment dependencies that aren't obvious until something breaks. When multiple teams work on shared infrastructure, unclear domain boundaries lead to merge conflicts and coordination overhead.
The fundamental question isn't just about file organization - it's about modeling infrastructure that reflects business architecture, deployment requirements, and team structure while avoiding coupling issues that make systems fragile.
Technical Requirements
A well-organized CDK project needs to address several technical requirements:
Deployment Independence: Teams should be able to deploy their infrastructure changes without coordinating with other teams or worrying about breaking unrelated systems.
Clear Ownership: Every stack and construct should have an obvious owner, making it clear who to ask when issues arise or changes are needed.
Maintainability: Infrastructure code should be easy to navigate, with related resources grouped logically so developers can find what they need quickly.
Scalability: The organization pattern should work as the project grows from 10 resources to 500, from 1 team to 10 teams, without major restructuring.
Cross-Cutting Concerns: Shared infrastructure like VPCs, security policies, and monitoring needs to be handled without duplication or awkward dependencies.
Organization Patterns
Service-Based Organization
The service-based pattern organizes code by AWS service type - all Lambda functions together, all DynamoDB tables together, all API Gateways together.
This approach seems logical initially - all Lambda functions are in one place, making it easy to apply consistent configurations. However, the problems become apparent quickly:
- Making a change to user functionality requires touching multiple directories and stacks
- Cross-stack references create deployment dependencies (DynamoDB stack must deploy before Lambda stack)
- Team ownership is unclear - who owns the "Lambda stack"?
- Deploying a subset of infrastructure (just user-related resources) is difficult
When service-based works: Small projects with fewer than 10 resources, learning/experimentation phase, or single-service applications where the organization pattern doesn't matter much yet.
Domain-Based Organization
The domain-based pattern organizes code by business domain or bounded context, grouping all infrastructure for a domain together.
This organization aligns infrastructure with business capabilities. Changes to user functionality only touch the user/ directory. Team ownership is clear - the user team owns the user stack. Domains can deploy independently (after shared infrastructure), and extracting a domain into a separate repository is straightforward if needed.
Single-Table Design compatibility: Domain-based organization pairs exceptionally well with DynamoDB Single-Table Design pattern. When a single table contains multiple entity types (User, Order, Payment), each domain can maintain its own access patterns and repository logic within its folder. For example, the user/ folder contains user-repository.ts with User-specific queries, while order/ folder has order-repository.ts managing Order access patterns - all using the same physical table. The table definition lives in shared/, while domains own only their access patterns.
When domain-based works: Microservices architectures, multi-team organizations, business-aligned infrastructure, Single-Table Design usage, or when clear bounded contexts exist.
Feature-Based Organization
For product-focused teams, organizing by user-facing features makes more sense than technical domains.
Feature-based organization works well when features span multiple AWS services and domains, teams are organized around product features rather than technical layers, and the product requires rapid feature development and deployment.
When feature-based works: Product companies with feature teams, rapid development environments, features that can be deployed independently.
Layer-Based Organization
Layer-based organization separates infrastructure by technical layer, grouping resources by lifecycle and change frequency.
Layer-based organization provides clear separation of stateful and stateless resources, different update frequencies handled separately (foundation changes rarely, APIs change frequently), reduced deployment risk since the data layer rarely changes, and easier implementation of different removal policies per layer.
The drawbacks are that business logic gets scattered across layers, team ownership is less clear, and deploying complete features requires coordinating across multiple layers.
When layer-based works: Clear infrastructure layers, separate infrastructure and application teams, need to separate stateful from stateless resources.
Hybrid Approach
Real-world projects often benefit from combining patterns - using layers for cross-cutting concerns while organizing business logic by domain.
The hybrid approach gives us the best of both worlds: foundation layer handles cross-cutting concerns, domain stacks maintain business logic cohesion, and clear dependency graphs enable independent deployment after foundation is established.
Decision Framework
When to Split Stacks vs Use Constructs
One of the most common questions I encounter is: "When should I create a new stack versus just using a construct?" The answer comes down to deployment independence.
Use multiple stacks when:
- Different teams will maintain different parts
- Different deployment schedules (data layer deploys monthly, compute layer deploys daily)
- Different environments or accounts (dev in one account, prod in another)
- Independent deployment is critical for CI/CD
- Approaching CloudFormation's 500 resource limit per stack
- Different business domains with clear boundaries
- Stateful resources need different removal policies than stateless resources
Use constructs (not stacks) when:
- Only separating code ownership within the same team
- Resources change together and should deploy together
- No need for independent deployment
- Resources have tight coupling and many references
- Organizing purely for code maintainability
Here's a decision tree example:
Monorepo vs Multi-Repo Strategy
The monorepo versus multi-repo decision affects how teams collaborate and deploy infrastructure.
Monorepo pattern:
Monorepo benefits include single source of truth, atomic changes across services, easier refactoring across boundaries, shared constructs without publishing to npm, simplified dependency management, and single CI/CD pipeline configuration.
The drawbacks are larger repository clone time, risk of tight coupling between services, CI/CD complexity detecting which services changed, less granular team permissions, and potentially larger deployment blast radius.
Multi-repo pattern uses separate repositories for each service, with shared constructs published to a private npm registry.
Multi-repo benefits include clear service boundaries, independent versioning and deployment, clear team ownership and permissions, smaller repository size, language/framework diversity, and reduced merge conflicts.
The drawbacks are cross-service changes requiring multiple PRs, shared constructs needing publishing and versioning, dependency version management complexity, harder to ensure consistency, and more CI/CD pipeline overhead.
My recommendation: Start with a monorepo for teams under 10 people. The simplicity of atomic changes and shared constructs outweighs the drawbacks at small scale. Split to multi-repo when team size or deployment independence demands it - usually around 50+ engineers or when you have truly independent services with different lifecycles.
Handling Cross-Cutting Concerns
Networking, security, monitoring, and other cross-cutting concerns don't fit cleanly into domain boundaries. Here are three patterns that work well.
Foundation Stack Pattern
CDK Aspects for Cross-Cutting Policies
CDK Aspects allow you to apply policies across all resources in your application automatically.
Shared Constructs Library
Create reusable L3 constructs that encapsulate best practices. You can further enhance these constructs with factory patterns and functional programming approaches - see AWS CDK Functional Patterns for details.
Multi-Environment Management
Managing multiple environments (dev, staging, prod) requires careful configuration strategy.
Static Stack Creation (Recommended)
Create all environments during synthesis to guarantee consistency.
This approach ensures that production configuration is validated during development. A typo in production config will be caught immediately when running cdk synth locally, not during production deployment.
Common Pitfalls
Premature Stack Splitting
Creating too many small stacks leads to excessive cross-stack references and deployment complexity.
Start with larger stacks and split only when there's a clear reason: different teams, different lifecycles, or approaching resource limits.
Circular Dependencies
Circular dependencies between stacks are a common issue when designing resource relationships.
Design unidirectional dependency flow. Use shared resource stacks or event-driven architecture to break cycles.
Cross-Account/Region References
Attempting to reference resources across AWS accounts or regions requires explicit handling.
Cross-account/region references require explicit export/import using SSM Parameter Store or manual ARN passing.
Not Using CDK Refactor Tool
Refactoring without preserving logical IDs causes resource replacement and potential data loss.
When reorganizing CDK code, always preserve logical IDs to prevent resource replacement.
Results
After implementing domain-based organization with a foundation layer in a project, the team saw measurable improvements. Deployment time for individual services dropped from 15 minutes (deploying all services) to 3-5 minutes (deploying just the changed service). Merge conflicts decreased significantly - instead of 3-4 conflicts per sprint from multiple teams touching shared stacks, conflicts became rare since each team owned their domain stack.
The organization pattern also improved onboarding. New developers could understand the user service by reading just the domains/user/ directory instead of piecing together infrastructure scattered across service-type folders. Code review time improved because reviewers could focus on a single domain without understanding cross-stack dependencies.
Key Takeaways
Organization pattern matches business structure: Choose domain-based for business-aligned teams, layer-based for infrastructure teams, feature-based for product teams. The pattern should reflect how your organization thinks about the system.
Constructs before stacks: Most code organization problems are solved with constructs, not multiple stacks. Split stacks only for deployment independence, team boundaries, or hitting resource limits.
Foundation pattern is essential: Separate shared infrastructure (VPC, networking, security) from domain-specific resources to avoid circular dependencies and enable domain independence.
Monorepo for small teams, multi-repo for large: Monorepos work well up to ~50 developers. After that, consider multi-repo for clear service boundaries and team autonomy.
Static stack creation guarantees consistency: Creating all environments during synthesis (dev, staging, prod) ensures tested code in dev is exactly what deploys to prod.
Start simple, evolve gradually: Begin with larger stacks and constructs, split into separate stacks only when clear reasons emerge (different teams, different lifecycles, resource limits).
The right organization pattern depends on your specific context - team size, deployment requirements, and business structure. Start simple and let the organization emerge as real needs become clear.
References
- docs.aws.amazon.com - AWS CDK Developer Guide.
- github.com - AWS CDK source repository and release notes.
- typescriptlang.org - TypeScript Handbook and language reference.
- github.com - TypeScript project wiki (FAQ and design notes).
- martinfowler.com - Martin Fowler: Domain-Driven Design tag index.
- docs.aws.amazon.com - AWS Overview (official whitepaper).
- cloud.google.com - Google Cloud documentation.