Distinguish declarative and imperative approaches precisely, explain how declarative tools compute and converge changes, recognise where imperative steps remain necessary, combine both safely, and choose between push-based and reconciling models
A declarative approach describes the desired end state ("there should be a VPC with three private subnets") and lets a tool work out what to create, change or delete. An imperative approach describes the sequence of actions ("create a VPC, then create subnet A, then …"). Declarative tools are idempotent and self-correcting by design; imperative scripts must handle ordering, existing state and failures themselves.
Figure 1 — Declarative vs imperative
Figure 2 — How declarative tools converge
Vocabulary
Meaning
Desired state
What the code declares
Convergence
Moving actual state toward desired
Reconciliation loop
Continuous convergence by a controller
Partial apply
Some changes succeeded before an error
Table 1 — Terms
2Why It Exists
Imperative pain
Declarative benefit
Scripts fail when resources already exist
Diff-based changes
Ordering bugs
Dependency graph
No preview
Plan
Cleanup forgotten
Removal from code deletes resource
Table 2 — Why
3Core Concepts — Declarative vs Imperative
Aspect
Declarative
Imperative
Author writes
End state
Steps
Ordering
Computed from dependencies
Manual
Re-run
No-op if converged
May duplicate/fail
Deletion
Remove from code
Write delete steps
Preview
Plan/change set
Dry-run if supported
Flexibility
Constrained by model
Unlimited
Table 3 — Comparison
Figure 3 — Spectrum of approaches
4How It Works Internally — Convergence
Step
Terraform
Kubernetes/Crossplane
Read desired
HCL
Custom resource spec
Read actual
State + provider refresh
Watch cloud resources
Diff
Plan
Controller compares
Act
Apply on command (push)
Continuously (pull)
Record
State file
status field
Table 4 — Two declarative models
4ADeep Dive — Same Change, Two Approaches
Task: add a third private subnet
Declarative (Terraform)
Imperative (script)
Code change
Add AZ to list
New create-subnet call with CIDR logic
Existing subnets
Untouched automatically
Must check they exist
Route table associations
Derived from for_each
Separate steps
Re-run safety
Yes
Must guard every step
Review artefact
"1 to add, 0 to change"
Script diff (behaviour unclear)
Table 5 — Declarative scales with complexity
4BDeep Dive — Where Imperative Still Fits
Task
Why imperative
How to contain it
Data migrations
Ordered, one-time
Versioned migration tools (Flyway)
Bootstrap (state bucket, first role)
Chicken-and-egg
Small documented script, then import
Operational actions (failover, restart)
Actions, not state
Runbooks / SSM Automation
Glue between systems
Sequencing
Pipeline steps calling declarative tools
Complex computed values
Logic
CDK/Pulumi generate declarative output
Table 6 — Keep imperative at the edges
4CDeep Dive — Push vs Pull Reconciliation
Model
Examples
Pros
Cons
Push (apply on demand)
Terraform in CI
Explicit plans, clear change points
Drift until next apply
Pull (continuous)
Crossplane, ACK, Argo CD
Self-healing
Harder previews, controller complexity
ShopNow choice
Push for AWS infra; pull (Argo CD) for Kubernetes apps
Best of both
—
Table 7 — Reconciliation models
5APIs / Features / Syntax
HCL
# Declarative: subnets derived from a map; adding an entry adds a subnetvariable "private_subnets" { type = map(object({ az = string, cidr = string })) default = { a = { az = "ap-south-1a", cidr = "10.20.16.0/20" } b = { az = "ap-south-1b", cidr = "10.20.32.0/20" } c = { az = "ap-south-1c", cidr = "10.20.48.0/20" } }}resource "aws_subnet" "private" { for_each = var.private_subnets vpc_id = aws_vpc.main.id availability_zone = each.value.az cidr_block = each.value.cidr}
BASH
# Imperative equivalent (fragile): must check existence, order and failuresfor az in a b c; do exists=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=private-$az \ --query 'length(Subnets)' --output text) [ "$exists" = "0" ] && aws ec2 create-subnet --vpc-id "$VPC" \ --cidr-block "$(cidr_for $az)" --availability-zone "ap-south-1$az"done
TYPESCRIPT
// CDK: imperative code that synthesises a declarative CloudFormation templateconst vpc = new ec2.Vpc(this, 'Main', { maxAzs: 3, subnetConfiguration: [{ name: 'private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 20, }],});
6Practical Examples
6.1 Script re-run created duplicates
A bash provisioning script re-run after a timeout created a second NAT gateway and a duplicate route table. The Terraform replacement had no such issue: re-apply was a no-op.
6.2 Forgotten resources
Imperative scripts created test queues that were never deleted (no delete path). Declarative definitions removed them when the code was removed; SQS cost dropped 12%.
6.3 CDK for complex logic
A team generating 200 similar IAM roles from a service catalogue used CDK loops in TypeScript — imperative authoring, declarative deployment via CloudFormation.
6.4 Worked example — idempotency test
Run
Declarative
Imperative
1
Creates
Creates
2
No changes
Errors or duplicates (unless guarded)
Table 8 — Re-runs reveal the difference
6.5 Worked example — deletion semantics
Action
Declarative
Remove resource block
Plan shows destroy
Guard
prevent_destroy for critical resources
Table 9 — Deletion by omission — review carefully
6.6 Worked example — ordering
Dependency
Declarative handling
Subnet needs VPC ID
Reference creates edge
IAM policy before role use
Reference or depends_on
Table 10 — Graph instead of manual order
6.7 Worked example — interview whiteboard
Prompt
Answer outline
"Declarative or imperative infrastructure?"
"Declarative describes desired state; the tool reads actual state, computes a diff (plan) and converges, making runs idempotent, ordered by dependency graphs and reviewable. Imperative describes steps; flexible but you own ordering, existence checks, partial failures and cleanup. Use declarative (Terraform/CloudFormation, or CDK/Pulumi generating declarative output) for infrastructure; keep imperative for migrations, bootstrap, operational actions and glue. Push (CI apply) vs pull (controllers) is a second axis."
Table 11 — Structured answer
6.8 Worked example — PayGrid preference
Rule
Detail
No imperative changes to CDE infrastructure
Only reviewed plans
Operational actions via approved SSM runbooks
Audited
Table 12 — Controlled imperative actions
6.9 Worked example — anti-patterns
Anti-pattern
Effect
local-exec provisioners for core resources
Hidden imperative steps
Mixing CLI changes with Terraform-managed resources
Drift
Huge conditional logic in HCL
Hard to read — move to code generation
Table 13 — Common mistakes
6.10 Worked example — provisioners
Guidance
Why
Avoid local-exec/remote-exec
Not idempotent, not in plan
Use user data, images, or configuration tools
Proper mechanisms
Table 14 — Last resort only
6.11 Worked example — partial failure
Scenario
Declarative recovery
Apply fails after 10 of 15 resources
State records 10; re-apply creates remaining 5
Table 15 — Resumable by design
6.12 Worked example — Kubernetes as declarative example
Resource
Behaviour
Deployment replicas: 5
Controller ensures 5 always
Table 16 — Continuous reconciliation
6.13 Worked example — Crossplane composition
Concept
Use
XRD + Composition
Teams request "Database" claims; controller creates RDS
Authoring is imperative; output (CloudFormation) is declarative
"Is Ansible declarative?"
Modules aim for idempotency, but playbooks are ordered steps
Table 22 — Nuances
6.20 Worked example — glue pipelines
Step
Type
terraform apply (network)
Declarative
Run DB migration
Imperative
argocd sync
Declarative (pull)
Table 23 — Pipelines orchestrate both
6.21 Worked example — error visibility
Model
Where errors appear
Declarative push
Plan/apply output in CI
Pull controllers
Resource status/events
Table 24 — Monitor both
6.22 Worked example — performance
Factor
Effect
Refresh of large states
Slower plans — split stacks
Imperative scripts
Fast but unsafe
Table 25 — Speed vs safety
6.23 Worked example — checklist
Item
✔
Declarative for infrastructure
✔
Imperative only for migrations/ops/glue
✔
No provisioners for core resources
✔
Plans reviewed
✔
Reconciliation model chosen per layer
✔
Table 26 — Approach baseline
6.24 Worked example — reading a plan summary
Summary line
Meaning
Plan: 1 to add, 0 to change, 0 to destroy
Declarative diff
No changes
Converged
Table 27 — Diff as communication
6.25 Worked example — CloudFormation change sets
Step
Detail
create-change-set
Preview
Review replacements
Replacement: True flags
execute-change-set
Apply
Table 28 — Declarative preview in CloudFormation
6.26 Worked example — drift in pull vs push
Model
Drift handling
Push
Detected at next plan/drift job
Pull
Corrected continuously
Table 29 — Choose per risk
6.27 Worked example — team skills mapping
Team
Preferred authoring
Platform (Ops background)
HCL
Application developers
CDK (TypeScript/Java)
Table 30 — Both produce declarative deployments
6.28 Worked example — CDK in Java
JAVA
Bucket media = Bucket.Builder.create(this, "Media") .versioned(true) .encryption(BucketEncryption.KMS_MANAGED) .blockPublicAccess(BlockPublicAccess.BLOCK_ALL) .build();
^Java teams can author infrastructure in Java
6.29 Worked example — imperative bootstrap script (contained)
Step
Detail
Create state bucket + lock
One-time script
Import into Terraform
Then managed declaratively
Table 31 — Chicken-and-egg solved once
7Common Problems & Mistakes
Mistake
Fix
Imperative provisioning
Declarative tools
Provisioner abuse
Images/user data
Out-of-band CLI edits
Pipeline-only
Logic-heavy HCL
Code generation (CDK)
Table 32 — Mistakes and fixes
Most problems here come from imperative habits leaking into declarative tooling. Teams keep provisioning scripts alongside Terraform, so some resources have two owners and nobody knows which is authoritative. Engineers fix incidents with console clicks or CLI commands on Terraform-managed resources, creating drift that the next apply silently reverts or that blocks the plan. Provisioners are used to install software instead of baking images. HCL is stretched with nested conditionals and complex for expressions until nobody can review it; at that point, generating declarative output from a real language with CDK is clearer. Deletion by omission is the last trap: removing a block destroys the resource, so every destroy line in a plan deserves a second look.
8Performance & Production Considerations
Consideration
Practice
Plan duration
Smaller stacks
Controller load (pull)
Rate limits
Partial failures
Re-apply/reconcile
Table 33 — Operating approaches
Declarative tools slow down as state grows, because every plan refreshes every resource through the provider APIs. A stack with thousands of resources can take many minutes to plan and hit API rate limits, so split state by layer and ownership, such as network, shared data and per-service stacks, and keep targeted applies for emergencies. Pull-based controllers have their own load: each reconciliation polls cloud APIs, so tune sync intervals and watch for throttling. Plan for partial failure as a normal event: declarative runs are resumable, so the response to a failed apply is to fix the cause and re-apply, not to repair resources by hand. Track plan duration and drift frequency as health signals for the infrastructure codebase.
9Architecture & Design Considerations
Guideline
Detail
Declarative core
Default
Imperative at edges
Contained
Choose push/pull per layer
Fit
Table 34 — Design guidance
10Interview Questions & Answers
Q1BasicConcept
What is declarative infrastructure?
Declarative infrastructure means writing down the desired end state, for example a VPC with three private subnets in three zones, and letting a tool work out how to get there. Tools such as Terraform, CloudFormation and Kubernetes compare the declaration with what actually exists, compute the difference, and create, change or delete only what is needed. The author models resources and their relationships rather than writing steps, so ordering comes from dependencies, re-running is safe, and removing a block from the code removes the resource. The tool's plan or change set becomes the review artefact. That predictability is why declarative tooling is the default for cloud infrastructure in most organisations.
Q2BasicConcept
What is imperative infrastructure?
Imperative infrastructure means writing the ordered steps that create or change resources: call create-vpc, then create-subnet for each zone, then attach route tables, usually in a shell script, SDK program or CLI sequence. It is flexible, because any logic can be expressed, but the author owns everything a declarative tool would handle: checking whether resources already exist, running steps in the right order, recovering from partial failure, avoiding duplicates on re-run and writing explicit delete steps. Without those guards, re-running a script after a timeout can create a second NAT gateway, as happened at ShopNow. Imperative approaches still fit migrations, bootstrap and operational actions, which are naturally sequences.
Q3IntermediateInternals
Why are declarative tools idempotent?
Because they act on the difference between desired and actual state, not on a list of instructions. On every run, Terraform reads the configuration, refreshes the real state of each resource from the provider APIs, compares the two and plans only the changes needed to converge. If everything already matches, the plan is empty and apply does nothing, so running it twice has the same effect as running it once. Kubernetes controllers do the same continuously in a reconciliation loop. Idempotency also makes partial failure recoverable: if an apply fails after ten of fifteen resources, state records those ten, and the next apply creates only the remaining five, without duplicates.
Q4IntermediateInternals
How does Terraform determine order?
Terraform builds a dependency graph from references between resources. When a subnet's vpc_id refers to aws_vpc.main.id, Terraform adds an edge meaning the VPC must exist before the subnet. It then walks the graph, creating independent resources in parallel, ten at a time by default, and destroying in reverse order. You never write the order yourself; it falls out of the data flow. When a dependency is real but not visible through attributes, such as an IAM policy that must exist before a service uses the role, you add an explicit depends_on. Overusing depends_on is a smell, because it hides why the order matters and serialises work that could run in parallel.
Q5Senior EngineerTrade-off
When is imperative appropriate?
Imperative is appropriate when the work is genuinely a sequence of actions rather than a state to maintain. Database schema migrations are ordered, one-time steps, best run with versioned tools such as Flyway or Liquibase. Bootstrap tasks, like creating the Terraform state bucket and the first role, are chicken-and-egg problems solved once by a small, documented script, after which the result is imported into declarative code. Operational actions, such as failover, restarts or scaling during an incident, are actions, not state, and belong in runbooks or SSM Automation. Glue between tools fits pipeline steps. Keep imperative work at the edges, versioned and reviewed, and never use it to create core infrastructure.
Q6Senior EngineerTricky
Is CDK declarative?
Partly, and the distinction is worth making. CDK authoring is imperative: you write TypeScript, Java or Python with loops, conditions, classes and functions. But the output is declarative. cdk synth runs your code to produce a CloudFormation template, and CloudFormation then performs a normal declarative deployment with change sets, dependency ordering, rollback and drift detection. Your program describes what the template should contain; it does not call AWS APIs to create resources step by step. That gives you general-purpose language features such as typing, unit tests and abstraction, which suit teams generating many similar resources, while keeping the safety of declarative deployment. Pulumi works in a similar way with its own engine.
Q7Technical ArchitectTrade-off
Push vs pull reconciliation?
Push reconciliation applies changes on demand, typically from a CI pipeline running terraform plan and apply. Its strengths are explicit plans, clear review and change points, and an audit trail for every apply; its weakness is that drift goes unnoticed until the next plan or a scheduled drift job. Pull reconciliation runs controllers that watch desired state and continuously converge reality towards it, as Argo CD, Crossplane and AWS Controllers for Kubernetes do. That gives self-healing and fast drift correction, but previews are harder and you must operate the controllers themselves. Many organisations combine them, as ShopNow does: push-based Terraform for AWS infrastructure and pull-based Argo CD for Kubernetes applications.
Q8Technical ArchitectTricky
Why avoid provisioners?
Terraform provisioners, local-exec and remote-exec, run arbitrary scripts during resource creation, which smuggles imperative steps into a declarative tool. Their effects do not appear in the plan, so reviewers cannot see what will happen. They are not idempotent and not tracked in state: if a provisioner fails, the resource is marked tainted and replaced on the next apply, and if it succeeds, Terraform never re-runs or reconciles it. They also create hidden dependencies on the machine running Terraform, such as installed tools and network access. Use proper mechanisms instead: baked images, user data or cloud-init, configuration management, or a separate pipeline step. HashiCorp itself describes provisioners as a last resort.
Q9Solution ArchitectDesign
How do you mix approaches safely?
Keep a declarative core and push imperative work to the edges, contained and orchestrated. All long-lived infrastructure is defined in Terraform, CloudFormation or Kubernetes manifests and changed only through reviewed plans. Imperative steps, such as database migrations, bootstrap scripts and operational actions, are versioned, idempotent where possible, and run by pipelines or SSM Automation rather than by people at a terminal. A pipeline sequences them, for example terraform apply, then a Flyway migration job, then an Argo CD sync. Ban out-of-band CLI changes to managed resources, because they create drift, and run drift detection regularly. PayGrid goes further, allowing no imperative changes to cardholder-data infrastructure except approved SSM runbooks.
Q10Solution ArchitectConcept
What's the review artefact for declarative changes?
The plan or change set. terraform plan, or a CloudFormation change set, shows exactly what will be added, changed, replaced or destroyed, computed against real state, so reviewers approve the effect of a change rather than guessing from a code diff. A summary such as 1 to add, 0 to change, 0 to destroy communicates intent, while any destroy or forced replacement deserves close attention, especially on stateful resources such as databases. In a pipeline, save the plan file, show it in the pull request, require approval, and apply exactly that saved plan so nothing changes between review and execution. Add prevent_destroy or policy-as-code checks on critical resources as a further safeguard.
Newsletter
Get new tutorials every week
New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.
Your subscription could not be saved. Please try again.
Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 9
Cloud, Containers, DevOps & Production Architecture