Infrastructure as Code with Java, Organized as Business Components
Overview
This architectural style applies the BCE pattern to AWS CDK v2 infrastructure written in Java:
infrastructure is built the
same way a business application is built: as business components, not as a flat pile of resources.
A construct is a reusable capability; a stack is a deployment boundary. Keeping that split makes the
infrastructure code readable, testable, and refactorable.
The result is a Maven, Java 25 CDK project whose packages are BCE business components. The entry-point
class (CDKApp) loads typed configuration, wires stacks in dependency order, and calls
app.synth(). Synthesis stays deterministic and side-effect free: no AWS SDK calls, no
randomness, no timestamps during construction.
Business Components
Business components are packages named after the AWS capability they provision:
s3, cloudfront, certificate,
route53, dynamodb, lambda,
cognito, codebuild, iam,
stepfunctions. When a component spans multiple services, it is named after its
domain responsibility instead of any single service.
The package path follows the familiar shape
[organization].[application].[businessComponent].[boundary|control|entity].
A component uses only the layers it needs: a stateless resource factory is a control-only
component (e.g. s3.control.Buckets); stacks from other components call it directly.
Explicit IAM roles and policy statements live in a dedicated iam component, and
Cognito always gets its own stack.
Boundary
Stacks are the boundaries. A stack class (extends Stack) is the CloudFormation deployment
boundary: it composes constructs, connects cross-stack dependencies, applies tags, and declares
outputs.
Thin Stacks: Stacks compose constructs and connect dependencies; resource configuration lives in controls
Independent: Never pass a Stack instance to another stack; pass resource interfaces (IBucket, ITable, IFunction)
Naming: Every stack class ends with Stack, never with Construct
Pinned Regions: A central Stacks interface exposes pinned StackProps constants, one per region
Derived Names: CloudFormation stack names are derived from configuration, not hardcoded
Control
Controls are reusable, stateless resource factories (bucket factories, a CodeBuild publishing
project) and helpers that connect existing resources: Route53 alias records pointing at a
CloudFront distribution, IAM grants, event subscriptions. They serve the same purpose as CDK
constructs (reusable capabilities) without necessarily subclassing Construct. Prefer
Java interfaces with static factory methods over utility classes.
Builder Pattern: Prefer Builder.create(scope, "LogicalId")…build() over constructor invocation
Highest-Level Constructs: Use L2/L3 constructs; drop to Cfn* (L1) only when no higher-level construct exists
Stable Logical IDs: Semantic PascalCase logical ids; they are deployment identity and are never derived from timestamps or randomness
Interfaces In and Out: Accept and return IBucket, ITable; consumers depend on behavior, not concrete classes
Least Privilege: Prefer grant* methods (bucket.grantReadWrite(role)) over hand-written policy statements
Protect State: RemovalPolicy.RETAIN for buckets and tables holding data, kept together in a separate stack so redeployments of frequently-changing stacks never touch them
Entity
Entities are immutable data describing infrastructure without creating resources: build specs, typed
value objects, and configuration records. Configuration is parsed once at the application entry point
and passed down as typed records; a stack never reaches back to the raw property source.
Records: Immutable Java records for configuration and value objects (e.g. a GitRepository record)
Domain Behavior: Records carry derived names, normalization, and withX copy methods rather than staying anemic
Build Specs: CodeBuild specs and similar declarative artifacts as entity factories
Single Source: CDK context and properties are read once in CDKApp, never from inside a stack
Principles
Stacks vs. Constructs
A stack is a deployment boundary; a construct is a reusable capability. Keeping the split
makes infrastructure readable, testable, and refactorable.
Deterministic Synthesis
No AWS SDK calls, no randomness, no timestamps during construction. The same source and
context always synthesize an equivalent template.
Least-Privilege IAM
grant* methods generate correctly-scoped policies. Explicit policy statements
are the fallback, kept in a dedicated iam component.
Generated Names
CloudFormation generates physical names: bucket, table, and role names are omitted unless
an operator-facing name is genuinely required.
Testing
Synthesis Tests: Build an App, instantiate the stacks, call app.synth(), and assert the template contains the expected resources, properties, and grants; test the synthesized contract, not a snapshot
Parameterized Entity Tests: Drive specs, ordering, and derived names with parameterized JUnit 5 tests; pure logic is cheaply verifiable without synthesis
Package-Private Access: Tests live in the same package as the code under test
Build & Deployment
Maven, Java 25: Core dependencies are aws-cdk-lib and constructs only; any further dependency needs an explicit justification
Entry Point: cdk.json runs the app via mvn -e -q compile exec:java
Cross-Region: crossRegionReferences(true) only on stacks consuming a resource from another region (e.g. CloudFront with an ACM certificate in us-east-1)
Helper Scripts: buildAndDeploy.sh and destroy.sh wrap the build and deployment lifecycle