IaC Functional Design
The Arda-cards/infrastructure repository organizes its CDK code as a layered architecture with a single, strict dependency direction. This page defines the layers, their responsibilities, and the rules that keep the codebase reusable, instance-pluggable, and safe to evolve.
This is the structural document. Mechanical conventions used inside a single layer (the Configuration / Props / Built three-interface pattern, ExportKeys, publish(), naming) live in the infrastructure repository’s knowledge-base/cdk-construct-patterns.md and are referenced from here rather than duplicated.
1. The dependency direction (the central rule)
Section titled “1. The dependency direction (the central rule)”script → instances → apps → stacks → constructsA → B means A imports from / depends on B, never the other way round. So instances/ files import from apps/, but no file under apps/ ever imports from instances/. There are no cycles inside constructs/, and no cycles anywhere in this hierarchy.
The arrow encodes a value chain. constructs/ define raw building blocks; stacks/ compose them; apps/ wire stacks together; instances/ pin the wiring to one specific deployment scenario; script/ is the entry point that ties an App definition and an instance value together and synthesizes or deploys.
2. Layer responsibilities
Section titled “2. Layer responsibilities”| Layer | Repository directory | Reusable? | Defines | Imports from |
|---|---|---|---|---|
constructs/ | src/main/cdk/constructs/ | Yes — many uses per construct | Per-construct Configuration interface, Built shape; CDK Construct subclass | Nothing app-specific (CDK + platform/) |
stacks/ | src/main/cdk/stacks/ | Yes — many uses per stack | Per-stack Configuration interface, CDK Stack subclass that composes constructs | constructs/, platform/, CDK |
apps/ | src/main/cdk/apps/ | Yes — pinned by one or more instances (varies by App) | Per-app Configuration (its own shape), App class wiring stacks together | stacks/, platform/, CDK |
instances/ | src/main/cdk/instances/ | No — pins values for one specific deployment | Concrete const values typed to an App’s Configuration | apps/ (for the type), platform/ (for value sources) |
script / tools | tools/, top-level entries referenced by cdk.json | No — entry points | Nothing structural | The App class + the instance value; ties them together; may orchestrate multiple Apps |
platform/ (a sibling, not a layer in this hierarchy) holds cross-cutting constants and small adapters that any layer may read — domain names, locator types, 1Password reference helpers, etc. It is the only thing constructs/ is allowed to know about other than the AWS CDK itself.
3. Layer organization — how the codebase is grouped
Section titled “3. Layer organization — how the codebase is grouped”3.1 Constructs — by function
Section titled “3.1 Constructs — by function”Constructs are organized by the kind of resource concern they encapsulate, not by which App or Stack uses them:
- storage/ — buckets, tables, secret stores
- compute/ — Lambda, Fargate, EKS-adjacent
- xgress/ — ingress and egress: CloudFront, NLB, API Gateway, custom domains
- dns/ — Route53 zones, NS-delegation writers
- secrets/ — Secrets Manager wiring, ESO bridges
- iam/ — roles, policies, authorization primitives
- observability/ — log groups, dashboards, alarms
Grouping by function (rather than by consumer) lets a new App pick up a construct without that construct having to know about it. A new App is allowed to import any construct; a construct may not know which Apps it ends up in.
3.2 Stacks — by runtime layer and type
Section titled “3.2 Stacks — by runtime layer and type”Stacks live under stacks/ in subdirectories that mirror the runtime layering of the platform:
stacks/root/— account-level bootstrapping; top-of-tree DNS (RootDnsStack).stacks/corporate/— Corporate-account resources (mail DNS forarda.ardamails.com, future HubSpot integration).- Platform-infrastructure stacks — shared infrastructure (VPC, IAM roles, baseline DNS) used by every partition in an account.
- Platform-partition stacks — per-partition product runtimes (dev / stage / demo / prod), each consuming the platform-infrastructure outputs.
A stack name reflects the concern it owns; the directory it lives in reflects which runtime layer that concern belongs to.
3.3 Apps — by role in the Arda Platform
Section titled “3.3 Apps — by role in the Arda Platform”Each App is a reusable definition; an instance pins it to a specific deployment scenario.
- Root — the top-of-tree DNS / bootstrap App. Single instance per account family.
- Product (
Al1xand similar) — runtime Apps for product partitions. One App definition, many instances (dev, stage, demo, prod, kyle, …). - Corporate — Corporate-account Apps (mail, future HubSpot). One App definition, one instance today; potentially more if a separate Corporate-style account is later added.
App-by-role makes the Configuration the App exposes match the way operators reason about the platform — “deploy the Root App”, “deploy the Corporate App for Alpha001”, “deploy a Product App for the dev partition”.
3.4 Instances — by top-level resource aggregation
Section titled “3.4 Instances — by top-level resource aggregation”There is one instance file per App, declaring the const values that pin that App. The granularity matches the App’s Configuration, not the asset within it. Instance files do not sub-divide per asset (no free-kanban-tool.ts next to mail-root.ts and a barrel index.ts when the App’s Configuration is a single shape — match the App).
3.5 Scripts — by function
Section titled “3.5 Scripts — by function”Scripts under tools/ are organized by the operator function they serve: synthesis entry points (one per App, referenced by cdk.json), drift checks, operator CLIs, deploy runners, multi-App choreographies. Scripts are the only layer that may know about more than one App.
4. What every reader must internalize
Section titled “4. What every reader must internalize”-
Apps, Stacks, and Constructs are reusable abstractions. They are parameterized by their
Configurationinterface. A single App definition can be instantiated with different pinned values by different instances (the dev variant of the Product App and its prod variant share the same App class). -
Instances are the only place where deployment-specific decisions live. They pin one value of an App’s Configuration per actual deployment scenario. Anything that varies per environment that is not derivable from a value already at a lower layer belongs in an instance file.
-
Scripts are the only place that calls
new cdk.App()(ornew MyApp(...)). Files underapps/define App classes; they have no side effects at module load. This is what makes Apps composable inside larger choreographies. The same discipline extends topublish(): stacks exposepublish()as a method but never call it from their own constructor — calling order is wired inapps/<App>/...afteraddDependency(). See the cross-stack export key reference for the mechanics. -
The App’s Configuration is its own contract, not a mechanical union of stack configurations. The App may:
- Aggregate — passthrough union, when applicable.
- Fix stack parameters internally — those parameters become App-level decisions, not user-facing knobs.
- Derive stack parameters from cross-stack outputs or
platform/constants. - Project one App-level field into multiple stack-level values.
The App’s Configuration is designed for its consumers (instance authors and script authors), not for the convenience of its stacks.
-
Instance → App is 1 : 1; App → instances is 1 : N. Each instance file pins exactly one App (it imports that App’s
Configurationtype). An App, in turn, is pinned by one or more instances. Single-deployment Apps (e.g. Corporate today, Root) have one instance; product runtime Apps have many — the Platform-Partition App is pinned bydev,stage,demo,prod,kyleinstances across two infrastructures (Alpha001, Alpha002), and gaining a new partition is solely the act of adding a new instance file. Reuse is the rule, not the exception. -
Script ↔ App is 1 : N. A script may instantiate multiple Apps in sequence — choreographed deployments, multi-app verifications, sequenced operator workflows. This is the only layer that gets to know about multiple App-shaped things at once.
-
Type-system discipline. Each layer’s
Configurationinterface lives at that layer. Never define a stack or app shape in an instance file; never let a construct depend on stack or app types.
5. Worked examples
Section titled “5. Worked examples”5.1 Root App — a stable, well-layered App
Section titled “5.1 Root App — a stable, well-layered App”instances/Root/dns.tsdeclares theconstvalues that pin the Root App for the canonical Arda account family (zone names, theardamails.comimport target, NS-delegation IAM role configuration).apps/Root/defines the Root App class and itsConfigurationinterface. The App class composes the Root stacks; it has no side effects at module load.stacks/root/RootDnsStack.tscomposes DNS-family constructs (Route53 hosted zones, theWriteNSRecordsToUpstreamDnsconstruct, theAllowCreatingNSRecordsRoleIAM role).- The
tools/runner script callsnew RootApp(instanceValue)and synthesizes / deploys.
5.2 Corporate App — a fresh App built to the same model
Section titled “5.2 Corporate App — a fresh App built to the same model”The Phase 3 email-integration delivery introduces a Corporate App against this exact shape:
apps/Corporate/definesCorporateAppwith its ownConfiguration(Corporate parent zone, sender list, verification-target DNS records).stacks/corporate/CorporateMailDns.tsandstacks/corporate/FreeKanbanToolMailDns.tsare composed byCorporateApp. Each Stack has its ownConfigurationshaped for its role.instances/Corporate/corporate.tsis the single instance file pinning Corporate-App values for the canonical Arda Corporate account.- A small entry script under
tools/synthesizes and deploys the Corporate App; the same script can later call into the Postmark CLI thin-wrapper for the operator-driven domain-verification leg.
5.3 Platform-Partition App — one App, many instances
Section titled “5.3 Platform-Partition App — one App, many instances”The Al1x Product App is the model case for App reuse:
apps/Al1x/defines the App class and its Configuration.instances/Al1x/dev.ts,instances/Al1x/stage.ts,instances/Al1x/demo.ts,instances/Al1x/prod.ts,instances/Al1x/kyle.tseach pin the App for a specific partition.- The
tools/cdk-runner.jsis data-driven over partition apps and selects the correct instance per CI matrix row.
6. Anti-patterns
Section titled “6. Anti-patterns”The dependency-direction rule rules out a recurring set of mistakes. Each is wrong because it inverts an arrow or smears a responsibility across layers.
- Defining a
Configurationinterface in aninstances/file. The shape belongs atapps/orstacks/— wherever the consumer of that shape lives. Instances are values, not types. - A stack importing from
instances/. The arrow goes the wrong way. The stack is reusable; the instance is the pinned value. If a stack needs information that today only an instance carries, lift the field into the App’s Configuration and pass it down. - An
apps/file callingnew cdk.App()at module load. The App class is reusable code; the script is the entry. Module-load side effects make Apps un-composable inside choreographies. - Per-asset instance files (
free-kanban-tool.ts,mail-root.ts,index.ts) when one file would do. Match the App’s granularity, not the asset’s. If the App’s Configuration is a single shape, the instance is a single file. - Thin-wrapper constructs that take the whole
cdk.Apponly to callapp.node.tryGetContext(key). Pass the resolved value, not the App. Constructs do not need to know they live in a CDK App at all — they take parameters. - Treating the App’s Configuration as a mechanical union of stack configs by default. Design the App’s Configuration for its consumers; aggregating, fixing, deriving, and projecting are all available. The “union” approach leaks every internal stack decision out to the instance author.
- Using
cd <path>in shell helpers that touch this codebase. Usegit -C <path>andmake -C <path>to keep the shell cwd stable and avoid the permission prompts that interrupt agent and operator workflows alike. (See path conventions for the workspace-wide rule.)
7. Enforcing the dependency direction
Section titled “7. Enforcing the dependency direction”Rules that are not enforced rot. Three practical mechanisms keep this layering honest, all live in Arda-cards/infrastructure:
- ESLint
import/no-restricted-pathsineslint.config.mjs. Three zones forbid the wrong-direction imports captured in § 2:constructs/cannot import fromstacks/,apps/, orinstances/;stacks/cannot import fromapps/orinstances/;apps/cannot import frominstances/. CDK App entry scripts (the only files that legitimately importinstances/) live undertools/, which is the script layer and is therefore not subject to the apps-layer rule. The CI lint stage andmake lintboth fail on any wrong-direction import. - Code-review checklist baked into
.github/PULL_REQUEST_TEMPLATE.md. A new construct must declare which kind-of-resource concern it encapsulates (storage / compute / xgress / dns / secrets / iam / observability). A new Stack must declare its runtime layer (root / corporate / platform-infrastructure / platform-partition). A new App must declare its role (Root / Product / Corporate / other). The template is the gate; reviewers confirm the boxes during review. - AWS-impact classification on every PR, also captured by the same template. Each PR ticks
None,Synth-only, orResource-touching. The layering helps the classification become mechanical: changes confined toconstructs/andstacks/are typically synth-only by inspection; changes toinstances/andtools/are the ones that produce different deployed states; resource-touching PRs require an explicitcdk diffreview.
8. See also
Section titled “8. See also”- Section index — what else lives in the Infrastructure Patterns section.
- Design Pattern Index — the catalog this section sits under.
- Implementation Patterns — code-level patterns; the construct’s
Configuration/Props/Builttriad is the IaC counterpart. - AWS CDK Infrastructure — technology reference covering the CDK primitives and repo-local conventions used inside these layers: the
Configuration/Props/Builtpattern, cross-stack export keys, custom-resource provider framework, removal policies,cdk importchoreography, CDK assertions, and common pitfalls. - The infrastructure repository’s
knowledge-base/cdk-construct-patterns.md— repo-local mechanical conventions referenced throughout this page.
Copyright: © Arda Systems 2025-2026, All rights reserved