A pattern I have found useful for small services, internal tools, and projects that benefit from fast iteration is to make the repository itself the lifecycle boundary for the service and its infrastructure.
The basic idea is simple:
Create the Git repository from a specific template, and the necessary infrastructure is provisioned along with it automatically.
I think of the resources associated with a repository as an infrastructure arena: an isolated set of environments, cloud resources, identities, Terraform state, deployment automation, and other service-specific machinery whose lifecycle is tied to that repository.
This is not a pattern I would claim is universally applicable. There are plenty of systems whose infrastructure have a longer lifecycles than any individual application repository, and plenty of organizations where shared resources, cross-service dependencies or more centralized control make sense.
But for the right class of workload, I have found this model makes experimentation and service development substantially cheaper.
The problem
Provisioning a new service usually involves significantly more work than developing the service itself.
Before the first useful line of application code is deployed, someone may need to create a cloud project or account, configure Terraform state, create identities and permissions, establish CI/CD, configure workload identity, create registries, set up deployment infrastructure, and wire all of those things together.
Platform teams often try to solve this by introducing increasingly high-level abstractions, which in many cases result in big modules, multiple infrastructure monorepos or automated workflows.
That can work, but those abstractions often come with a different cost: the platform begins to dictate what an application is supposed to look like. They are usually designed around what infrastructure teams think is an “average” service, yet very few services are actually completely average in practice. The result is often infrastructure that only approximately fits the workload: overprovisioned resources, unnecessary components, and design choices preserved primarily for the sake of uniformity. Opting out may be technically possible, but doing so often means leaving the intended golden path, taking on additional work, and having to justify decisions that would otherwise be perfectly reasonable for the service at hand.
I believe that automating the repetitive parts aggressively while keeping the underlying infrastructure accessible is more scalable, faster to iterate with and often leads to an overall better development experience.
The paved road should be easy to enter, lightweight by default, and deliberately unopinionated about what a service should look like.
Instead of starting from a large abstraction designed around an imagined average service, the base layer should provide only the essentials that are guaranteed to be common across all services: sensible defaults, security guardrails, lifecycle management, and a working deployment path. From there, teams can opt into additional capabilities as their service requires them.
Those capabilities should themselves be reusable and composable: databases, queues, deployment patterns, observability, workload-specific templates, and other infrastructure pieces that can be stacked together as needed rather than imposed upfront.
The result is closer to a set of interchangeable and reusable components than to a fixed application framework. Start with the smallest universally useful unit as a foundation, add the pieces that actually fit the workload, and retain the ability to build something different when the provided pieces do not.
The repository as the lifecycle root
In this model, engineers create services from repository templates.
A minimal template might be called something like google-cloud-project. Creating a repository from that template is enough to bootstrap the fundamental infrastructure needed by the project.
A GitHub Actions workflow in the template repository triggers a centrally maintained reusable bootstrap workflow when a new repository is created.
Detecting repository creation is slightly more awkward than it should be. There is a create event, but in my experience it does not fire reliably for repositories instantiated from a template. What has worked consistently is triggering on push and gating the bootstrap job on github.event.created, which the push payload sets to true for the initial push into a new repository. Organization-level webhooks or a GitHub App also work and are likely the more robust option at scale, at the cost of running infrastructure outside Actions.
That workflow has access to a tightly scoped bootstrap identity and establishes things such as:
isolated Terraform state
project-level cloud resources
service identities
IAM bindings to federated workload identities and deployment permissions
the normal Terraform execution path
whatever minimum organizational guardrails are required (although these should preferably be inherited from the account or folder level whenever possible)
After bootstrap, the repository manages its own infrastructure through ordinary Terraform.
The obvious objection is that this makes repository creation a privileged operation. If creating a repository provisions cloud projects and identities, what stops a repository from provisioning whatever it wants?
The answer is that the bootstrap identity is not bound to the repository. It is bound to the workflow. GitHub’s OIDC token includes a job_workflow_ref claim identifying the reusable workflow that produced it, and workload identity federation can condition on that claim rather than on repository name. The trust policy says, in effect: I will issue this identity only to tokens minted by this centrally maintained workflow, at this ref.
A newly created repository can therefore invoke the bootstrap and get exactly what the bootstrap grants. It cannot write its own workflow that assumes the same identity, because its own workflow produces a different job_workflow_ref. The privilege lives in code the application team does not control, invoked from a repository they do.
The important part is that each repository gets its own state and its own isolated security boundary. One experimental service does not share a Terraform state file with ten unrelated services. Its infrastructure can be modified, broken, recreated, or destroyed independently.
The repository effectively becomes the root object from which the service hangs. Admittedly, tearing down a service is somewhat less straightforward: reacting to repository deletion requires either a GitHub App or an organization-level webhook capable of receiving the repository deletion event.
Infrastructure allocation as an arena
The analogy I like is an arena allocator. In it, instead of managing the lifecycle of each object independently, many individual allocations are associated with a larger lifetime.
The same idea can be useful for cloud infrastructure.
A developer experimenting with a service should not need to maintain a mental inventory of every type of infrastructure provisioned along with their service: cloud resources, mailing lists, Slack channels, GitHub teams, etc.
Those resources belong to the service arena, which makes experimentation considerably less intimidating.
Composable, rather than prescriptive
The other part of the model that I think matters is composability.
The default template should be minimal, unopinionated and lightweight. It provides good defaults and establishes the lifecycle and security boundaries, but it does not have to become a bespoke internal application framework.
Higher-level templates can then build on top of it. For example, I might have a cloud-run-service-go template built on top of the basic Google Cloud project template. In addition to the normal project bootstrap, it includes:
a small Hello World application written in Go
Terraform for the Cloud Run service
the resources required to build and deploy the application
a ready-to-use CI/CD pipeline
Creating a repository from that template gives me a working Cloud Run service almost immediately. There is no separate infrastructure provisioning step, no Terraform backend setup, and no need to assemble a deployment pipeline before testing an idea.
But importantly, the template remains a starting point.
If I want a database or queue, I can compose with another centrally maintained Terraform module. Or, if an existing module does not fit my use case, I can define the Terraform resources directly. And if I do not want the higher-level Cloud Run template at all, I can still start from the bare-bones cloud project and build something entirely different.
The abstraction is therefore progressive:
modules → minimal project template → workload-specific templates
Each layer provides additional convenience, but none needs to hide or replace the layer below it.
There is a major difference between a platform that says, “Here is the easiest way to do this,” and one that says, “This is the only way you are allowed to do this.”
I strongly prefer the former.
Centralized machinery, decentralized (but clear) ownership
This pattern also creates what I think is a useful division of responsibility.
Platform or infrastructure teams can centrally maintain the pieces that benefit from standardization:
bootstrap workflows
Terraform modules
identity configuration
security controls
CI/CD primitives
organization-wide defaults
Application teams still own their repositories and can evolve their infrastructure independently.
The goal should not be to completely eliminate infrastructure knowledge or operational work from application teams, but rather to eliminate infrastructure boilerplate and ceremony.
There is a meaningful difference between understanding that your Cloud Run service has an identity, a Terraform state, and a deployment pipeline, and having to manually construct all three every time you start a project.
Security implications
This model also changes where some of the most important security boundaries live, and it’s important to keep this in mind when adopting this type of platform architecture.
First, the ability to create a repository may now imply the ability to provision infrastructure. If repository creation can automatically trigger bootstrap workflows with privileged cloud identities, repository creation is no longer just a source-control permission. It becomes a general infrastructure operation.
That may be perfectly acceptable, but it should be treated deliberately. Organizations that need tighter control can place additional authorization checks in the GitHub App or bootstrap workflow, restrict which users or teams may create repositories from infrastructure-enabled templates, or host these repositories in a separate organization with more restrictive creation policies.
Second, branch protection becomes a core part of the infrastructure change management security model. Merge rights become deployment rights.
Once Terraform and deployment configuration live inside the repository, the ability to modify protected branches can effectively become the ability to modify the service’s infrastructure. In many cases, changing the Terraform means changing almost everything: identities, permissions, network configuration, databases, deployment targets, and the application itself.
Repository protections therefore become real access controls rather than merely development-process safeguards. Required reviews, CODEOWNERS, protected branches, restricted force pushes, and appropriate controls around automation credentials all matter considerably more under this model.
The same is true of the repository containing centralized workflows. Reusable workflows capable of assuming privileged identities form an important part of the organization’s software and infrastructure supply chain. Changes to those workflows should be treated accordingly: tightly controlled write access, protected branches, careful review, pinned dependencies where appropriate, and strict validation of any inputs that cross the trust boundary between an application repository and a privileged reusable workflow.
The convenience of repository-scoped infrastructure does not eliminate privileged operations. But it does move them into a smaller number of explicit and auditable control points, and those need to be protected as such.
Why I like this model
The main advantage, in my experience, is speed.
The cost of trying and iterating on an idea becomes very low. A developer can create a repository, immediately get an isolated and deployable environment, iterate normally, and discard the entire thing when the experiment is no longer useful.
At the same time, the organization keeps important guardrails around authentication, identity, state isolation, deployment, and resource ownership.
This is a combination that is often surprisingly hard to achieve. Fast developer environments sometimes become loosely governed collections of manually created cloud resources. Highly governed environments sometimes turn every new service into a platform request. Repository-scoped infrastructure provides a useful middle ground.
Not every repository is a service
This pattern should not be interpreted as saying that every repository must correspond to a service. Many repositories clearly do not: shared libraries, documentation, tooling, Terraform modules, reusable workflows, and monorepos are obvious examples.
The point I’m trying to make is that a repository is often a very good lifecycle and ownership boundary for an independently operated service.
For many internal services in particular, I have found little reason to introduce another abstraction between the repository and the workload if the repository model itself remains sufficiently flexible. A service can own its application code, infrastructure definition, deployment configuration, identities, and operational metadata in one place without requiring a separate platform-controlled object to represent it.
There will always be exceptions. Some systems contain several independently deployed services in one repository. Others span several repositories while still representing a single operational system. Shared infrastructure may have a lifecycle entirely independent from application code. A shared production database serving twenty applications should obviously not disappear because one repository was deleted.
However, coupling infrastructure lifecycles across service boundaries should be a deliberate decision, made with a clear ownership model and lifecycle plan. Personally, I would also try to avoid architectures where resources are grouped primarily by their provider (such as a particular cloud or SaaS platform) and their lifecycle is managed collectively. Whenever possible, I prefer resources to follow the lifecycle of the services that actually depend on them.
The core idea is not to force a one-to-one mapping between repositories and services everywhere. It is to recognize that, where the repository already represents the natural unit of ownership and change, it can also serve as the infrastructure lifecycle root.
Cheap creation changes behavior
Perhaps the most important effect of this pattern is not technical.
When services are expensive to create, developers naturally avoid creating them. They extend existing systems, share resources that should not really be shared, or postpone experiments because provisioning another environment feels like too much work.
But when a deployable service is roughly as cheap to create as a repository (and just as easy to discard), the calculus changes. Experiments can actually be experiments. Small services can have small overhead. And infrastructure can become something developers compose when needed rather than something they accumulate and eventually have to clean up.



