Freedom paradox and virtue of constraints

The Paradox of Freedom in Software Development

We love freedom. Modern software development, fueled by agile practices, has given us more autonomy than ever before. Teams crave independence—our code, our infrastructure. Any interference feels like an attack on our velocity.

But here’s a hot take: too much freedom can be harmful. At certain stages of development, endless choices become a burden. Not every team should be picking their runtime or cloud provider. You don’t get to choose between Kubernetes, ECS, or Swarm.

Why? Because constraints matter. The sheer number of choices—especially in areas where dev teams aren’t experts—becomes overwhelming. Do developers really need to worry about the nuances between different instance types and generations? No. Should we first pick our preferred cloud vendor? No. Should we spend days/weeks setting up yet another CI/CD pipeline? No. Development teams should focus should be on building features, not wrestling with infrastructure details.

Ironically, this is exactly why people love Heroku. The contract is simple: expose a port, receive HTTP traffic. That’s it. No need to think about disk throughput or Kubernetes CPU allocations. Heroku imposes extreme constraints, yet developers embrace Heroku because it eliminate unnecessary complexity. Within those boundaries set by the platform, teams still have the freedom to choose their tech stack—as long as they follow the contract. Just git push and your software is deployed and running.

The same freedom we crave often pulls us away from what matters: delivering value. We get lost in infrastructure tinkering, solving problems that don’t move the business forward.

The contract between teams and platforms has grown too large. We need to embrace constraints—not as limitations, but as enablers of creativity where it truly counts.

Architecture and constraints

Heroku is a excellent example of how constraints force better architectural decisions. By limiting what developers can control, it nudges teams toward simpler, scalable, and maintainable applications. You can’t take shortcuts. This aligns closely with the 12-Factor App methodology, which promotes best practices for cloud-native software.

Codebase → One Repo, One App

A single application is tied to a single Git repository.

Impact: Encourages monorepo discipline or a clear separation of services, preventing tangled dependencies.

Dependencies → Explicit and Isolated

No global dependencies; everything must be declared in package.json, requirements.txt, go.mod, etc.

Impact: Forces proper dependency management and prevents “it works on my machine” issues.

Config → Stored in Environment Variables

Configuration (e.g., DB credentials, API keys) must be in environment variables, not hardcoded.

Impact: Leads to 12-factor compliant config handling, makes applications stateless and easily portable. *However!#### I will argue in later posts that you should avoid environment variables but we will get to that on a later date.

Backing Services → Treat as Attached Resources

Databases, queues, and caches are external services

Impact: Loosely coupled architecture, making it easier to swap or scale dependencies.

Build, Release, Run → Strictly Separated

The build pipeline ensures that code, config, and dependencies are compiled into slug/container before being run.

Impact: Eliminates “snowflake servers/installations” and ensures reproducible, immutable deployments.

Processes → Stateless and Share-Nothing

Each dyno/container is ephemeral. No local state persistence.

Impact: Forces applications to be horizontally scalable and use external storage for state. Shedding state which is difficult to maintain drives simple services.

Port Binding → Self-Contained Services

Apps must listen on a dynamically assigned $PORT

Impact: Enforces proper service encapsulation, making it easy to deploy in any environment and prevents applications from clashing.

Concurrency → Scale via Process Model

Scaling is done by adding dynos/processes/containeres, not by modifying a single instance.

Impact: Encourages process-driven scaling, making apps resilient and fault-tolerant. Makes scaling easier.

Disposability → Fast Startup & Graceful Shutdown

Dynos/containers can be restarted anytime; apps must handle SIGTERM properly.

Impact: Improves resilience and reliability, preventing downtime from ungraceful shutdowns.

Dev/Prod Parity → Keep Environments Synced

Local development is designed to mimic production.

Impact: Reduces “works in dev, breaks in prod” issues by keeping environments consistent.

Logs → Event Streams

Logs are aggregated and streamed; no log files allowed.

Impact: Enforces centralized logging for better monitoring and debugging. Makes integrations such as SIEM systems easier.

Admin Processes → Run as One-Off Tasks

Admin tasks (e.g., migrations) run like application. Not as part of the application processes/containers.

Impact: Prevents long-lived manual processes, ensuring operational consistency.

Conclusion

Heroku’s rigid constraints force teams to follow 12-factor principles. This eliminates huge amount of bad architectural habits and therefore makes applications:

  • ✅ More scalable
  • ✅ More resilient
  • ✅ More portable
  • ✅ Easier to deploy and operate

By embracing constraints, teams focus on delivering features, not fighting infrastructure. Heroku’s simplicity and constraints aren’t limitations — instead they are blueprints for better software design!

Achieving the same benefits with platform engineering

Achieving the Same Benefits with Platform Engineering

Similar benefits can be achieved without Heroku by implementing strong platform engineering practices. The key is to enforce clear contracts between teams and infrastructure while abstracting away unnecessary complexity.

  • ✅ Kubernetes with GitOps (e.g., ArgoCD, Flux) – Automates deployments while enforcing consistency
  • ✅ Internal Developer Platforms (IDPs) (e.g., Backstage, Humanitec, Port) – Standardizes developer workflows
  • ✅ Containerized Runtimes (e.g., Fly.io, AWS App Runner, Google Cloud Run) – Simplifies deployment with Heroku-like constraints
  • ✅ Infrastructure as Code (IaC) (e.g., Terraform, Pulumi, Crossplane) – Ensures repeatable, consistent infrastructure
  • ✅ Service Mesh (e.g., Istio, Linkerd) – Handles service-to-service communication without app-level concerns
  • ✅ Centralized Logging & Observability (e.g., OpenTelemetry, Grafana, Loki) – Provides visibility without per-app log handling

By designing opinionated, developer-friendly platforms, organizations can give teams the right level of freedom while enforcing best practices — just like Heroku does.