Application vs. Infrastructure: Two Things You Deploy

I treated “deploy” as one thing, push the container and it goes live, until a deploy failed for a reason that had nothing to do with my code: the new service version expected a database that did not exist yet. My application deploy had no idea about the infrastructure it depended on, because I had never separated them. The app assumed an environment that someone was supposed to have provisioned, and “someone” was a gap. Deploying the app and provisioning what it runs on are two different jobs, and conflating them means each silently assumes the other is handled. This lesson draws the line my earlier deploys never made me draw.

The last lesson showed why click-ops infrastructure is the “works on my machine” failure moved up one layer: an environment clicked together in a console months earlier could not be reproduced in a second region because the recipe was never written down. The fix was to move that infrastructure into code. But there is a prior question that the click-ops lesson assumed and never named: which parts of a running service are even infrastructure in the first place, and which parts are the application that runs on top of them. The first lesson made one infrastructure resource into code: a single resource block that apply created. That resource is a different kind of deployable than the container the earlier cloud-deployment work pushed onto a platform. This lesson is about the boundary between the two: where it falls, why a staff engineer holds the two apart, and what specifically breaks when they are collapsed into one step.

The word “infrastructure” here means what it meant in the first lesson: the resources a service runs on (the host, the network, the database, the secrets store) as distinct from the application code that runs in them. The application is the container or code being shipped. Provisioning is the act of standing up the infrastructure; deploying is the act of putting the application onto it. Those two terms carry the whole lesson, so they are worth holding precisely from the start.

I treated “deploy” as one thing, push the container and it goes live, until a deploy failed for a reason that had nothing to do with my code: the new service version expected a database that did not exist yet. My application deploy had no idea about the infrastructure it depended on, because I had never separated them. The app assumed an environment that someone was supposed to have provisioned, and “someone” was a gap. Deploying the app and provisioning what it runs on are two different jobs, and conflating them means each silently assumes the other is handled. This lesson draws the line my earlier deploys never made me draw.

The last lesson showed why click-ops infrastructure is the “works on my machine” failure moved up one layer: an environment clicked together in a console months earlier could not be reproduced in a second region because the recipe was never written down. The fix was to move that infrastructure into code. But there is a prior question that the click-ops lesson assumed and never named: which parts of a running service are even infrastructure in the first place, and which parts are the application that runs on top of them. The first lesson made one infrastructure resource into code: a single resource block that apply created. That resource is a different kind of deployable than the container the earlier cloud-deployment work pushed onto a platform. This lesson is about the boundary between the two: where it falls, why a staff engineer holds the two apart, and what specifically breaks when they are collapsed into one step.

The word “infrastructure” here means what it meant in the first lesson: the resources a service runs on (the host, the network, the database, the secrets store) as distinct from the application code that runs in them. The application is the container or code being shipped. Provisioning is the act of standing up the infrastructure; deploying is the act of putting the application onto it. Those two terms carry the whole lesson, so they are worth holding precisely from the start.

“Deploy” is two jobs: provision the infra, deploy the app

The decision this section forces is one most engineers never consciously make: how much of what a service runs on does a platform create invisibly, versus what is declared explicitly as code? It matters because the answer sets which failures are even possible: a hidden dependency that surfaces only when it is missing, versus an explicit one a reviewer can catch before it bites. The plausible-but-wrong model a competent mid-level engineer holds is that “deploy” is a single operation: a git push, the platform builds the image, and the running service appears. That model is not wrong because it is unsophisticated; it is wrong because it hides a boundary, and a hidden boundary is one nobody can reason about until a deploy fails on the side that was forgotten.

Here is the model breaking. A platform that does everything on git push looks like one job, so an engineer writes a deploy step that assumes the database is simply there.

# The mental model: "deploy" is one step. push -> live.
def deploy(image_tag: str) -> None:
    platform.build(image_tag)          # build the application image
    platform.run(image_tag)            # run the container
    # the app, on boot, connects to the database it assumes exists:
    #   conn = connect("scorer-db.internal:5432")
    # but nobody ran the step that CREATES scorer-db.internal.
    # the platform auto-provisions a container and a URL, not an
    # arbitrary database. so this boots, then dies on first query:
    #   OperationalError: could not translate host name "scorer-db.internal"

The deploy “succeeded”: the container is running. The failure shows up on the first request, reads as an application error (a connection failure deep in the code), and has an infrastructure cause (the database was never provisioned). The single-step model cannot even express the bug, because in that model there was no separate provisioning step to forget. The correct model is that “deploy” hides two operations with different inputs, different blast radii, and different correct frequencies: provisioning the resources a service runs on, and deploying the application onto them. They are not two phases of one job; they are two jobs that happen to be triggered near each other. A platform that performs both on every push does not merge them; it hides the boundary.

The choice, then, is how much of the infrastructure to let the platform hide versus declare explicitly. There are three genuine positions, and the right one depends entirely on whether the application needs anything the platform will not hand it for free.

Managed platform (PaaS): infra hidden

When: the application’s needs fit what the platform auto-provisions (a container, a URL, maybe a managed database add-on) and a small team wants to ship, not run infrastructure. A managed platform, or PaaS, works at a higher abstraction than a single host: it takes a technology-specific artifact (a container image, a Java WAR, a Ruby gem) and runs it without the operator ever naming the host it runs on. This is the correct early call; the cloud provider controls the underlying hardware and software infrastructure, and the operator controls only the application and its data.

Failure modes: the first time the application needs a resource the platform will not create (a specific network, a message queue, a second region) the deploy hits a wall with no escape hatch. The more nonstandard the application, the more likely it is the PaaS does not expose the internals to fix it. Until that wall, the infrastructure dependencies are invisible, so a missing one surfaces at deploy time as an application failure with an infrastructure cause: exactly the opening incident.

Explicit infrastructure-as-code: infra visible

When: the application needs resources beyond the platform’s defaults, more than one environment is required, or the infrastructure has to be reviewed and reproduced. The infrastructure becomes a declared, version-controlled artifact: the same kind of file the first lesson applied for one resource, now describing the resources the service assumes.

Failure modes: real adoption cost (a tool, a state concept the next lesson is built around, a learning curve). Over-provisioning infrastructure the service does not yet need is its own waste: declaring everything on day one is the opposite mistake from hiding it all, and it is as expensive in a different currency.

Hybrid: PaaS for the app, IaC for the resources around it

When: the common real answer. Let the platform run the container, but declare the database, the DNS records, and the secrets store as code so those dependencies are explicit and reproducible while the application still deploys fast.

Failure modes: a split-brain if the boundary is unclear. If both the platform config and the IaC config can set the same environment variable, they drift: each “fixes” the value and overwrites the other, and which one wins depends on deploy ordering. Name the ownership boundary explicitly (which side owns each resource and each config value) or the two halves disagree silently. The clean way to keep that boundary straight is to remember that configuration lives at three separate layers that must not overlap: build config declares how the artifact is built (pyproject.toml / the lock file, owned by the repo), infrastructure config declares the resources that must exist (the IaC files here, owned by whoever provisions), and runtime config is the per-environment values the running service reads (the typed settings object the serving module loads at startup, injected by the platform). A value belongs to exactly one layer; the split-brain drift above is what happens when two layers both claim the same one.

The decision rule is to default to the platform while the application fits it, and pull a resource into IaC the moment it becomes a dependency that must be reproduced or reviewed. The principle holds in every column: infrastructure dependencies should be declared, not assumed. How much to declare is the trade-off, and the signal to declare more is the day the team says “we needed this twice and could not reproduce it.” That is the same scale signal from the click-ops lesson (more than one environment, more than one person) landing now on the boundary instead of on the console.


Try It 1

The deploy below is the single-step model from this section. Reading it, predict which line fails at runtime and whether the failure reads as an application bug or an infrastructure bug. Then fill in the two-job version: a provision step that lists what the platform does not create, and a deploy step that depends on it.

python
"""Try It 1 starter: split the single-step deploy into two jobs.

Predict which line fails at runtime and whether it reads as an app bug or an infra
bug. Then fill in the two-job version: a provision step that lists what the platform
does NOT create, and a deploy step that depends on it.
"""


def single_step_deploy(image_tag: str) -> str:
    # the app boots and connects to a database it assumes exists.
    # the platform auto-provisions only a container + a URL.
    # which assumption fails, and does it read as app or infra?
    return "guess: line ___ fails, reads as a(n) ___ bug"


def provision() -> list[str]:
    # return the infra the platform will NOT create for free.
    return ["TODO: name the resources the app assumes"]


def deploy(image_tag: str, provisioned: list[str]) -> str:
    # the app deploy depends on provision() having run first.
    return "TODO: state the dependency on provisioned resources"


print(single_step_deploy("scorer:v2"))
print("provisioned:", provision())
print(deploy("scorer:v2", provision()))
Hint The platform hands you a container and a URL. The application's first query needs something the platform did not hand it. Re-read the "Managed platform" tab: which resource is the one a PaaS will not auto-create, and at what moment does its absence become visible?

Solution

The completed version splits the one step into a provision step that lists what the platform does not create and a deploy step that asserts against it. Watch the dependency become a checked line that fails before the deploy rather than after.

python
"""Try It 1 solution: naming the boundary turns an assumption into a checkable fact.

The single-step model could not express the bug, because there was no provisioning
step to forget. Splitting the job in two makes the dependency a line of code that
fails loudly before the deploy instead of a connection error that fails quietly after.
"""


def single_step_deploy(image_tag: str) -> str:
    # the container runs, but the first DB query fails: the database
    # host was never provisioned. it reads as an app error (a
    # connection failure in code) with an infra cause (no database).
    return "the DB connect line fails at runtime; reads as an app bug, infra cause"


def provision() -> list[str]:
    # the resources the platform will not create for free -- the
    # application assumes these into existence.
    return ["scorer-db (database)", "private-network", "db-credentials (secret)"]


def deploy(image_tag: str, provisioned: list[str]) -> str:
    # deploy is a separate job that DEPENDS on provision having run.
    assert "scorer-db (database)" in provisioned, "infra missing: provision first"
    return (
        "deployed "
        + image_tag
        + " onto "
        + str(len(provisioned))
        + " provisioned resources"
    )


provisioned = provision()
print(single_step_deploy("scorer:v2"))
print("provisioned:", provisioned)
print(deploy("scorer:v2", provisioned))

The single-step model could not express the bug at all, because in it there was no provisioning step to forget. Splitting the job in two makes the dependency a line of code (assert ... in provisioned) that fails loudly before the deploy instead of a connection error that fails quietly after it. That is the entire payoff of naming the boundary: the assumption becomes a declared, checkable fact.

Where the line falls: what outlives a deploy

Before this section, the application and its infrastructure are one undifferentiated “deployment” (a single push that makes everything appear) and there is no way to point at where one ends and the other begins. The mid-level instinct is to draw the line by ownership or org chart: infrastructure is whatever the platform team owns, the application is whatever sits in the service repo. That instinct breaks the moment a deploy has to touch only one side. A routine code change should not be able to delete a database; a database resize should not have to ride along with an application rollback. The collapsed model cannot answer the question those cases force: which of these gets replaced right now, and which must survive?

Watch the ownership-based line fail. Both of these “belong to the app team” and live in the same deploy, yet they behave oppositely on the next deploy.

# Drawing the line by ownership: both are "ours", so deploy both together.
app_team_owns = {
    "container_image": "scorer:v2",      # rebuilt and replaced every deploy
    "scorer_database": "postgres:5432",  # the data MUST survive every deploy
}

def deploy_everything(things: dict[str, str]) -> None:
    for name, spec in things.items():
        recreate(name, spec)   # tear down, stand back up: fine for the image...
        # ...catastrophic for the database: recreate() wipes the data.
        # ownership said "treat them the same". lifetime says the opposite.

The ownership line says treat them the same; recreating both is fine for the image and data loss for the database. Ownership is the wrong axis. The correct test is durability across a deploy: anything the application assumes is already there and that persists when a new version ships is infrastructure; anything thrown away and recreated on a ship is the application. A database is infrastructure because the data must survive the next deploy. The container image is application because the next deploy replaces it, and immutability is the reason: once an image is built it does not change, so the running application is replaced wholesale rather than mutated in place. Drawing the line by lifetime is what makes the dependency direction visible: the application points at the infrastructure (“I connect to this database”), and the infrastructure does not point back; it merely has to exist first.

This boundary is the synthesis of two things already established in earlier modules rather than a fact from any single source. The reproducibility work treated the image as a replaceable artifact, rebuilt and swapped on every deploy. The next lesson treats the tool’s state as a durable record worth protecting. The lifetime test composes those two: the application is the replaceable side, the infrastructure is the durable side, and the line falls exactly where one stops being thrown away and the other starts being kept.

The diagram below is the static structure of that relationship: two layers, with the dependency arrow that the PaaS hides.

rectangle "APPLICATION\n(container / code / image)\nreplaced on every deploy" as app #2d4a52
rectangle "INFRASTRUCTURE\n(host · network · database · secrets store)\noutlives every deploy" as infra #1f3a40
app --> infra : assumes into existence\n(provisioned first)
note right of infra : nothing here depends\non which app version runs

Read the diagram by the arrow: the application depends on the infrastructure existing, and nothing about the infrastructure depends on which application version is running. A PaaS draws this exact picture but greys out the lower box: the infrastructure is still there; the student never had to name it, which is precisely why a missing infrastructure resource feels like an application bug. This is also why the boundary is load-bearing for the rest of the module: the first lesson’s applied resource sits on the infrastructure side, which is exactly why the next lesson can treat its state as a durable record worth protecting and worth worrying about when it drifts. The application side has no such state to keep; it is reconstructed from the image every time.


Try It 2

A teammate has tagged each piece of a running service, but tagged it by who owns it: “platform team” or “app team.” Re-tag each piece by the lifetime test instead: does it survive the next deploy (infrastructure) or get replaced by it (application)? Fill in the lifetime field and the boolean for each.

python
"""Try It 2 starter: re-tag each piece by the lifetime test, not by owner.

A teammate tagged each piece of a running service by who owns it. Re-tag each piece
by the lifetime test instead: does it survive the next deploy (infrastructure) or get
replaced by it (application)? Fill in the lifetime field and the boolean for each.
"""


def classify(piece: dict[str, object]) -> dict[str, object]:
    # set survives_deploy True for things that must persist, then
    # lifetime = "infrastructure" if it survives else "application".
    return piece


pieces: list[dict[str, object]] = [
    {
        "name": "container_image",
        "owner": "app team",
        "lifetime": "TODO",
        "survives_deploy": None,
    },
    {
        "name": "user_database",
        "owner": "app team",  # owned by the app team, yet infrastructure by lifetime
        "lifetime": "TODO",
        "survives_deploy": None,
    },
    {
        "name": "secrets_store",
        "owner": "platform team",
        "lifetime": "TODO",
        "survives_deploy": None,
    },
    {
        "name": "request_handler",
        "owner": "app team",
        "lifetime": "TODO",
        "survives_deploy": None,
    },
]

for p in pieces:
    print(classify(p))
Hint Ignore the owner column entirely; it is the wrong axis, and that is the point of the exercise. For each piece ask one question: if the next deploy ships a new version, is this thing thrown away and recreated, or does it stay exactly as it was? Re-read the durability test in this section.

Solution

The completed function re-tags each piece by the lifetime test (survives the next deploy is infrastructure, replaced by it is application) and ignores the owner column entirely. Watch the owner tag and the lifetime tag land on opposite sides of the line.

python
"""Try It 2 solution: the boundary is lifetime, not the team's console.

The owner column and the lifetime column disagree -- a platform-team tag and an
app-team tag both land on either side of the line. The two pieces replaced by a
deploy are safe to recreate; the two that survive are the two a careless recreate
would destroy.
"""

replaced_by_deploy = {"container_image", "request_handler"}


def classify(piece: dict[str, object]) -> dict[str, object]:
    survives = piece["name"] not in replaced_by_deploy
    piece["survives_deploy"] = survives
    piece["lifetime"] = "infrastructure" if survives else "application"
    return piece


pieces: list[dict[str, object]] = [
    {
        "name": "container_image",
        "owner": "app team",
        "lifetime": "",
        "survives_deploy": None,
    },
    {
        "name": "user_database",
        "owner": "app team",  # the app team owns it, yet it is infrastructure by lifetime
        "lifetime": "",
        "survives_deploy": None,
    },
    {
        "name": "secrets_store",
        "owner": "platform team",
        "lifetime": "",
        "survives_deploy": None,
    },
    {
        "name": "request_handler",
        "owner": "app team",
        "lifetime": "",
        "survives_deploy": None,
    },
]

for p in pieces:
    c = classify(p)
    print(c["name"], "->", c["lifetime"], "(owner tag was:", str(c["owner"]) + ")")

The owner column and the lifetime column disagree: a platform team tag and an app team tag both land on either side of the line. That disagreement is the lesson: the boundary is conceptual, set by what survives a deploy, not by which team’s console it lives in. The two pieces that get replaced are the two that are safe to recreate; the two that survive are the two a careless recreate would destroy.

The cost of collapsing them: three failures of one-step deploy

A one-step deploy is not wrong because it is unsophisticated; it is wrong past a specific scale, and naming three failure modes is how to know that scale has been reached. The mistaken view is that letting the platform “do everything” removes the application’s dependence on a database, a network, and a secret. It does not remove the dependence; it hides it. The underlying principle is that an undeclared dependency is a dependency nobody can reason about, and hiding a dependency converts every infrastructure problem into a surprise at the worst possible time. Separating the deployables does not add the dependency; it makes the dependency that already exists legible before it fails. Three distinct failures become possible when the boundary is collapsed, and each looks like an unrelated bug until the missing boundary is seen as their common cause.

The first failure is invisible dependencies, the opening incident. With the two collapsed, the application’s infrastructure dependencies are nowhere written down; the application simply assumes a database, a network, a secret is present. That works until a new application version needs a resource nobody provisioned, and the deploy fails for an “infra” reason that reads as an “app” bug. The mechanism is concrete: deploying an application can automatically trigger provisioning of a service it requires, so in the collapsed model the reliance is real even when it is never recorded. Separated, the infrastructure config says “this database exists,” the application config says “I connect to it,” and a missing dependency is a visible gap in a diff before apply, not a runtime surprise.

The second failure is mismatched change cadence. Infrastructure changes rarely and dangerously (a database resize, a network rule) while application code deploys many times a day. The reason this matters is mechanical, not stylistic: a larger, slower-to-test infrastructure stack has a slower test-fix-test cycle and more content to break, so each infrastructure change is both riskier and slower to verify, which is exactly why infrastructure is changed incrementally, one change at a time. When the two share one deploy step, every routine application push drags the risky infrastructure change along, or an urgent application hotfix is gated behind an infrastructure review it should never have needed. Different kinds of change happen at different speeds and on different timelines and warrant different review; holding the deployables apart lets each move on its own rhythm: the application fast and often, the infrastructure slow and reviewed.

The third failure is shared blast radius. When the application and infrastructure are one step, a mistake in either can take down both: an application deploy that re-runs infrastructure provisioning can destroy a stateful resource in order to push a code change. That is the destroy-and-recreate the next lesson is built around, now triggered by a routine application deploy that had no business touching infrastructure. Aligning the deployment boundary to a single workload limits the blast radius of a change to one concern, the way compartments on a ship keep one flooded section from sinking the whole vessel. An application rollback then touches only the application, and an infrastructure change is reviewed against the much higher stakes of infrastructure. The reduction in blast radius is the whole reason to draw the boundary where the lifetime test drew it.

The code below makes the third failure concrete: a collapsed deploy that re-runs provisioning on every push, and the cost when a routine code change rides over a stateful resource.

python
"""The third failure made concrete: a collapsed deploy re-runs provisioning every push.

When a routine code change rides over a stateful resource, recreating it to ship a
code change destroys its data. The separated deploy touches only the app, leaving the
data exactly where it was. The collapse did not add the dependency -- it removed the
boundary that kept a code change from reaching the data.
"""


def collapsed_deploy(image_tag: str, infra: dict[str, str]) -> dict[str, str]:
    # one step: every app push re-runs infra provisioning. recreating a
    # stateful resource to ship a CODE change destroys its data.
    recreated = {name: "FRESH-EMPTY" for name in infra}  # destroy + recreate all
    return recreated


def separated_app_deploy(image_tag: str, infra: dict[str, str]) -> dict[str, str]:
    # app deploy touches only the app. infra is left exactly as it was.
    return dict(infra)  # untouched -- blast radius is the app alone


live_infra = {"scorer-db": "10,000 rows", "secrets": "set", "network": "configured"}

print("collapsed (routine code push):", collapsed_deploy("scorer:v3", live_infra))
print("separated (routine code push):", separated_app_deploy("scorer:v3", live_infra))

The collapsed deploy turned a routine code push into an empty database; the separated deploy left the data exactly where it was. The collapse did not add the database dependency; the dependency was always there. It removed the boundary that kept a code change from reaching the data. Restated as a signal: collapse is fine for one application on one platform-provided environment; the moment there is more than one environment, more than one person changing things, or any need to reproduce or audit, the cost of the hidden boundary exceeds the cost of declaring it. That threshold is the same “works on my machine” line the reproducibility module drew for the application, now drawn for the infrastructure it runs on.


Try It 3

Three deploys each touch a live environment that holds a stateful database. For each, decide whether collapsing the application and infrastructure into one step makes it dangerous, and name which of the three failure modes (invisible dependency, mismatched cadence, shared blast radius) it triggers. Fill in the risk and failure_mode fields.

python
"""Try It 3 starter: name the failure mode each collapsed deploy triggers.

Three deploys each touch a live environment that holds a stateful database. For each,
decide whether collapsing app and infra into one step makes it dangerous, and name
which of the three failure modes (invisible dependency, mismatched cadence, shared
blast radius) it triggers. Fill in the risk and failure_mode fields.
"""


def assess(d: dict[str, str]) -> dict[str, str]:
    # set risk to "dangerous" or "safe" and name the failure mode.
    return d


deploys: list[dict[str, str]] = [
    {
        "what": "routine code change, deploy re-runs provisioning",
        "risk": "TODO",
        "failure_mode": "TODO",
    },
    {
        "what": "new app version needs a queue nobody provisioned",
        "risk": "TODO",
        "failure_mode": "TODO",
    },
    {
        "what": "urgent app hotfix gated behind an infra review",
        "risk": "TODO",
        "failure_mode": "TODO",
    },
]

for d in deploys:
    print(assess(d))
Hint Each row maps to exactly one of the three failures named in this section. Ask of each: does the harm come from a dependency that was never written down, from a slow risky change being dragged by a fast one (or the reverse), or from a code change being able to reach a stateful resource? Re-read the three named failures.

Solution

The completed function marks each of the three deploys risky and names the single failure mode it triggers. Watch each row map to exactly one of invisible dependency, mismatched cadence, or shared blast radius.

python
"""Try It 3 solution: three deploys, three different failures, one common cause.

All three are dangerous and each fails differently: one destroys data, one fails on
absence, one stalls an urgent fix. They look like three unrelated incidents until you
see the collapsed boundary as the single common cause -- which is why separating the
deployables fixes all three at once.
"""


def assess(d: dict[str, str], verdict: tuple[str, str]) -> dict[str, str]:
    d["risk"], d["failure_mode"] = verdict
    return d


deploys: list[dict[str, str]] = [
    {
        "what": "routine code change, deploy re-runs provisioning",
        "risk": "",
        "failure_mode": "",
    },
    {
        "what": "new app version needs a queue nobody provisioned",
        "risk": "",
        "failure_mode": "",
    },
    {
        "what": "urgent app hotfix gated behind an infra review",
        "risk": "",
        "failure_mode": "",
    },
]

verdicts: list[tuple[str, str]] = [
    ("dangerous", "shared blast radius -- a code push can destroy the stateful DB"),
    ("dangerous", "invisible dependency -- surfaces only when the queue is missing"),
    ("dangerous", "mismatched cadence -- fast app fix stuck behind slow infra review"),
]

for d, v in zip(deploys, verdicts):
    a = assess(d, v)
    print(a["risk"].upper(), "->", a["failure_mode"])

All three are dangerous, and each fails differently: one destroys data, one fails on absence, one stalls an urgent fix. They look like three unrelated incidents until the collapsed boundary becomes visible as the single common cause, which is why separating the deployables fixes all three at once rather than three separate patches.


Summary

  • “Deploy” hides two jobs with different inputs, blast radii, and correct frequencies: provisioning the infrastructure a service runs on, and deploying the application onto it. A platform that does both on one push does not merge them; it hides the boundary.
  • The line between application and infrastructure falls by lifetime, not ownership: infrastructure outlives a deploy and is assumed into existence; the application is replaced on every deploy. The dependency arrow points application → infrastructure and never back.
  • Default to a managed platform while the application fits it; pull a resource into IaC the moment it becomes a dependency that must be reproduced or reviewed. The hybrid (PaaS for the app, IaC for the resources around it) is the common real answer, and its risk is a split-brain when the ownership boundary is unnamed.
  • Collapsing the two deployables makes three failures possible: invisible dependencies (surface only on absence), mismatched change cadence (a risky infra change dragged through every routine push, or an urgent fix gated behind an infra review), and shared blast radius (a code change able to destroy a stateful resource).
  • Collapse is fine for one app on one platform-provided environment; past more than one environment, more than one person, or any need to reproduce or audit, the cost of the hidden boundary exceeds the cost of declaring it.

Check your understanding:

  • What two distinct jobs does the word “deploy” hide, and what failure appears when the application’s dependency on infrastructure is assumed rather than declared?
  • Without looking back: by what test is a piece of a running service classified as infrastructure or application, and why is “which team owns it” the wrong test?
  • An apply for an unrelated change re-runs provisioning and empties a database. Which of the three failure modes is that, and what boundary would have prevented it?

This lesson is part of Pro

The Ship a Machine Learning Product path — every lesson, capstone, and the failure modes free tutorials skip. Sign in if you already have Pro, or unlock it below.

Unlock with Pro Sign in