04. Backing services
1. The rule: treat backing services as attached resources
The factor is one sentence: treat backing services as attached resources. A backing service is anything your app talks to over the network as part of normal operation: a datastore like Postgres or MySQL, a cache like Redis or Memcached, a message queue like RabbitMQ, an SMTP server for outbound mail, a third-party API for payments or maps or metrics. An attached resource is one the app reaches through a handle, a URL plus whatever credentials it needs, rather than something wired into the code.
That handle is config, which makes this factor the other half of Config. Factor 3 says the handle, the database URL and its credentials, belong in the environment rather than the source. Factor 4 picks up from there: given that the handle lives in config, treat whatever sits behind it as a resource you attach to the running app, not a fixed part of the app itself. As the series introduction put it, twelve-factor treats every backing service the same way, as an attached resource you point at with a URL from config; this is the factor that spells that out.
2. One code path, local or third-party
The heart of the factor is this: the app’s code makes no distinction between a service it runs itself and one a third party runs for it. A database on a box you manage and a database a vendor manages for you are the same kind of thing to the code, an attached resource behind a handle. The only thing that differs between them is the value of that handle, and the handle lives in config, out of the code’s sight. The same DATABASE_URL=<connection-string> in the environment points at a box you run in one deploy and at a managed instance in another; the code that reads the variable and opens a connection is byte-for-byte identical across both.
So the two factors split one idea between them. Factor 3 owns where the handle lives: in the environment, injected per deploy. Factor 4 owns what the handle points at: something loosely coupled to the app, swappable without touching code. Together they mean the app depends on the shape of a backing service, a database that speaks a protocol, a cache that stores keys, rather than on any one instance of it. It knows there is a database; it does not know, and does not care, who runs it.
3. The swap test
Here is the test for whether you are actually treating a service as attached: could you swap it for a different instance by changing only the handle in config, with no code change? Move self-hosted MySQL to a managed instance on Amazon RDS, or a local Redis to a managed cache, and if all that changes is the connection string in the environment, the service was loosely coupled the way this factor wants. If the swap forces you to edit code, it wasn’t, and something about that particular service had leaked into the app. For a same-engine swap the leak is usually a vendor-specific client library or a proprietary auth handshake; on a cross-engine move, say MySQL to Postgres, it is a query only one dialect understands. Once that is baked in, the swap that should have been a config change becomes a porting project.
The same looseness runs the other way. Because a resource is attached rather than built in, an operator can detach one and attach another at will. If a database starts misbehaving, you can detach it, bring up a fresh one restored from last night’s backup, repoint the handle, and restart, with no code change and no rebuild. Replacing the resource is an operations task, not a development one, because the resource is loosely coupled to the deploy.
Each distinct service is its own resource. Two databases are two resources even if they run the same engine; a database and the cache in front of it are two resources; the payments API and the maps API are two more. Each has its own handle and attaches and detaches on its own. What counts is the number of distinct services the app talks to, not the number of machines or vendors behind them.
4. The idea that became cloud-native
This is the factor that aged best, because its coupling model went on to dominate cloud infrastructure. It didn’t invent that model, though. Twelve-factor came out of Heroku, whose whole business was attachable managed add-ons, and the 2011 write-up already named managed and third-party backing services as examples: S3 for storage, Postmark for outbound mail, New Relic and Loggly for metrics, consumer APIs like Twitter and Google Maps. Managed services existed; the factor generalized the coupling they already ran on.
What the intervening decade changed is scale, not kind. Those early examples were a handful; today almost everything an app depends on is a managed backing service behind a handle: Amazon RDS for a relational database, S3 for object storage, managed Redis or Kafka for caching and streaming, and a long tail of SaaS APIs, a payment provider, an auth provider, a metrics backend, each one a URL and a key in your config. Every one is an attached resource in exactly the sense Factor 4 meant. You provision it, you get back a handle, you put the handle in the environment and the app talks to it without knowing that a vendor runs it. The factor didn’t predict managed services; it described the coupling model they would go on to adopt.
5. “Attached” doesn’t mean “invisible”
The one place the 2011 wording overreaches is its claim that the code “makes no distinction between local and third party services.” At the level of code, the claim holds: the same connection logic runs byte-for-byte whether you host the service or a vendor does. The overreach is the implication that identical code makes the network invisible, which it does not. Swapping the handle changes the URL, but it doesn’t carry over the three things that actually bite: the auth mechanism the new service expects, the connection pooling it needs, and its failure and latency semantics. Moving a database from the same box to a managed instance across a network link means the calls can now be slow, can time out, and can fail while the rest of the app keeps running, so a remote resource needs the treatment a remote call has always needed: timeouts, retries, and a way to fail gracefully, the core of software resilience. The config handle makes the service swappable; it does not make the network go away.
So separate the durable idea from the overreach. What lasts is the loose coupling: a backing service reached through a config handle, swappable without a code change, the same in code whether you run it or a vendor does. What overreaches is the idea that the network is invisible. It never is, and the fallacies of distributed computing are the standing reminder that treating a remote resource as a local one is a bug waiting for load.
Swappability is the prize, and it is a large one. Treat every backing service as an attached resource and the app stops owning any of them; its databases, caches, and queues all live outside it, behind handles it can repoint at will. What is left is a small, portable app that carries no state of its own and keeps its data in the resources it attaches to, which is most of what Processes asks for. That is the factor: reach your backing services through config, keep them at handle’s length, and you can move, replace, or restore any of them without ever touching the code.