Every developer remembers the moment they first heard the phrase “Don’t call us, we’ll call you.” It’s usually delivered with a knowing smirk by a senior engineer who has just watched a junior import React for the first time and try to wrestle control of the render loop. That phrase is the canonical, almost cliché, definition of a framework. But if you stop there, you’ve missed the entire point. The difference between a library and a framework isn’t a dry taxonomy—it’s a fundamental architectural decision that dictates how your brain works, how your codebase evolves, and how much you’ll suffer when the tool’s assumptions no longer match your reality.

I’m Kai Lindström, and I’ve spent the better part of two decades writing code that ranges from bare-metal C for embedded systems to sprawling TypeScript monoliths. I’ve seen teams pick the wrong abstraction and pay for it in blood, sweat, and merge conflicts. The library-versus-framework debate isn’t about semantics. It’s about who owns the main loop. It’s about inversion of control. And if you don’t understand the political implications of that inversion, you’re not engineering—you’re just typing.

The Technical Distinction: Who Holds the Reins?

Let’s strip away the marketing fluff. A library is a collection of functions and classes that you call from your code. You are the composer. You decide the flow, the structure, the sequence of operations. You import a library, invoke its methods, and handle the return values. Your code is the master; the library is the servant. A framework inverts this relationship. The framework provides the skeleton, the main loop, the lifecycle. You fill in the blanks. You write code that the framework calls. The framework is the master; your code is the servant.

This is not a spectrum. It’s a binary distinction based on a single property: inversion of control. If your code imports a module and calls its functions, it’s a library. If you write modules that are loaded and called by an existing runtime structure, it’s a framework. jQuery is a library. You call $('.element').hide() when you decide. Angular is a framework. You write a component class, and Angular decides when to instantiate it, when to call ngOnInit, and when to destroy it. You don’t call Angular; Angular calls you.

This distinction is not about size, complexity, or even dependency. A library can be massive and sprawling, like OpenCV. A framework can be relatively minimal, like Express.js. Wait—Express.js? That’s where the debate gets interesting, and where most developers get it wrong.

The Express.js Litmus Test: A Library Masquerading as a Framework

Express.js is often called a “web application framework” for Node.js. But let’s look at the control flow. In a typical Express app, you create an instance of an Express server, you define routes, you attach middleware, and then you explicitly call app.listen(port). You start the server. You decide the order of middleware. You are in control. Express provides a thin layer of routing and request/response abstractions, but it does not own the event loop. It does not dictate your project structure. It does not call your code on its schedule. Express is a library. A very useful, well-designed library, but a library nonetheless.

Compare this to something like Ruby on Rails or NestJS. In Rails, you don’t write a script that starts the server. You run rails server, and the framework boots up, scans your directory structure for models, controllers, and views, and wires them together based on naming conventions. You don’t call the framework; the framework discovers and calls your code. That’s inversion of control. That’s a framework.

Why does this matter? Because when you use a library, you can swap it out. You can replace Express with Fastify or Koa with relatively little pain, because your application’s architecture is your own. When you use a true framework, you are marrying its architecture. Divorce is expensive. Your business logic becomes entangled with the framework’s lifecycle hooks, dependency injection containers, and module system. You’re not just using a tool; you’re living in its house, by its rules.

Developers collaborating on a whiteboard, mapping out software architecture decisions

Code as Argument: The Library Style

Let’s make this concrete. Here’s a Python snippet using the requests library—a classic library example:

import requests

def fetch_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"API error: {response.status_code}")

# I decide when to call this. I own the flow.
user = fetch_user_data(42)
print(user["name"])

Notice the structure. I import the library. I write a function that uses it. I call that function when I want. The library is a tool I pick up and put down. If I decide tomorrow that requests is too slow and I want to use httpx for async support, I change the import and tweak the function. The rest of my application doesn’t care. My architecture is intact.

Code as Argument: The Framework Style

Now consider a Django view. Django is a framework. It’s often called an MTV (Model-Template-View) framework, and it owns the request/response cycle completely.

# views.py
from django.http import JsonResponse
from django.views import View

class UserDetailView(View):
    def get(self, request, user_id):
        # I don't call this method. Django's URL dispatcher does.
        user = User.objects.get(pk=user_id)
        return JsonResponse({"name": user.name})

# urls.py
from django.urls import path
from .views import UserDetailView

urlpatterns = [
    path("users/<int:user_id>/", UserDetailView.as_view()),
]

I never instantiate UserDetailView. I never call its get method. I register it in a URL configuration file, and Django’s machinery invokes it when an HTTP request matches the pattern. The framework provides the main loop (the WSGI server, the middleware stack, the URL resolver). I provide callbacks. If I want to switch from Django to, say, FastAPI, I’m not just changing imports. I’m rewriting the entire structural layer of my application—URL routing, request parsing, response serialization, authentication middleware. The framework’s architecture is my application’s architecture.

The Hidden Cost: Architectural Entropy

Frameworks promise productivity. They deliver it, initially. Rails can generate a fully functional CRUD app in minutes. Spring Boot can wire up a microservice with astonishing speed. But that initial velocity comes with a long-term tax: architectural entropy. As your application grows, it inevitably develops requirements that don’t fit the framework’s golden path. Maybe you need a complex, multi-step transaction that spans several HTTP requests and doesn’t map neatly to a single controller action. Maybe you need a custom authentication flow that doesn’t fit the framework’s middleware model. Maybe you need to integrate with a legacy system that speaks a protocol the framework doesn’t understand.

In a library-based architecture, you handle these edge cases by writing more of your own code, using the library where it helps and bypassing it where it doesn’t. In a framework-based architecture, you fight the framework. You override internal methods. You monkey-patch. You abuse hooks meant for entirely different purposes. You end up with code that is neither idiomatic framework code nor clean custom code—it’s a grotesque hybrid, full of comments like “// HACK: bypassing the normal lifecycle because…” and “// DO NOT REMOVE: this empty method prevents a crash in the parent class.”

I’ve seen a Rails application where the team, desperate to implement a long-running business process that didn’t fit the request/response model, ended up embedding a state machine inside ActiveRecord callbacks. The result was a Lovecraftian horror of after_save hooks triggering other hooks that triggered background jobs that, in turn, triggered more hooks. The framework’s inversion of control, once a helpful guide, became a straitjacket.

The Library-First Mindset: A Saner Approach

I advocate for what I call a library-first architecture. This doesn’t mean you never use frameworks. It means you treat frameworks as a last resort, not a default. Start with libraries. Build your own main loop, your own lifecycle, your own structure. Use libraries for specific, well-defined tasks: HTTP clients, database drivers, serialization, logging. Only introduce a framework when the complexity of managing those tasks yourself outweighs the cost of surrendering control.

This approach forces you to understand your problem domain deeply. You can’t hide behind a framework’s conventions. You have to make explicit decisions about threading, error handling, and data flow. That’s harder upfront, but it pays off in a codebase that is modular, replaceable, and free of hidden coupling. When a library becomes outdated or problematic, you swap it. When a framework becomes outdated—and they all do, eventually—you face a rewrite.

Consider the JavaScript ecosystem. React is often called a framework, but it’s a library. It doesn’t own the main loop; you integrate it into your own application structure. You can use React for just one component on a page, or for an entire single-page application with a router and state management library of your choice. That library-like flexibility is why React has outlasted full-fledged frameworks like Ember.js, which imposed a much stricter architecture. Ember’s inversion of control was total: it owned the router, the data layer, the component lifecycle, and the build pipeline. When its conventions fell out of favor, migrating away was a multi-year ordeal for many teams.

A developer refactoring code on a large monitor, symbolizing the cost of framework lock-in

The Political Analogy: Framework as Government

Here’s a mental model I find useful. A library is like a tool you rent from a hardware store. You use it for a job, then return it. A framework is like a government. It provides services—roads, schools, a legal system—but it also imposes taxes, regulations, and a constitution. You can’t easily opt out of one part without destabilizing the whole. And when the government becomes corrupt or obsolete, revolution is the only option. That revolution is a rewrite.

This is why I’m deeply skeptical of “full-stack frameworks” that try to own everything from the database to the DOM. They promise a unified developer experience, but they deliver a monoculture. Monocultures are efficient until a blight hits. In software, the blight is usually a shift in requirements or a new platform constraint that the framework’s architects didn’t anticipate. A library-first architecture is a diverse ecosystem. It’s messier, but it’s resilient.

When a Framework Actually Makes Sense

I’m not an absolutist. There are domains where a framework’s inversion of control is not just acceptable but desirable. The clearest example is game engines. Unity and Unreal Engine are frameworks in the purest sense. They own the main loop—the game loop—which handles physics simulation, rendering, and input polling at a fixed or variable timestep. You write scripts that are called within that loop. You don’t want to own the game loop. The complexity of cross-platform rendering, memory management, and real-time physics is so immense that surrendering control is a rational trade-off. The framework’s architecture aligns perfectly with the problem domain: every game is a loop that updates state and renders frames. There’s no edge case where you need to “bypass the game loop.”

Another valid case is embedded UI frameworks for specific platforms. If you’re writing an iOS app, UIKit (or SwiftUI) owns the event loop. You don’t fight it because the platform itself—the touchscreen, the gesture recognizers, the app lifecycle—is inherently framework-shaped. The inversion of control is imposed by the operating system, not just the library authors. Fighting it would mean fighting the platform, which is a losing battle.

But for most backend services, data pipelines, and even many frontend applications, the problem domain is not inherently framework-shaped. It’s just code that needs to respond to events, process data, and produce output. You can own that loop. You should own that loop.

FAQ: Sharpening the Distinction

Isn’t React a framework? It has hooks and lifecycle methods.

React is a library that provides a component model with its own internal lifecycle, but it does not own the application’s main loop. You integrate React into your application; you decide when to render, how to route, and how to manage state. The hooks and lifecycle methods are local to the component tree, not global to the application. You can use React alongside other libraries without conflict. A true framework like Angular owns the entire application lifecycle, from bootstrapping to destruction, and dictates the module structure, dependency injection, and routing. React’s inversion of control is partial and opt-in; Angular’s is total and mandatory.

Can a library become a framework over time?

Yes, and this is a dangerous evolution. A library can gradually accumulate framework-like features—convention-based file loading, auto-discovery of modules, a CLI that generates project scaffolding. When these features become the expected way to use the tool, and deviating from them breaks the tool’s assumptions, the library has metastasized into a framework. Next.js is a fascinating case study. It started as a thin layer on top of React (a library) but has grown to own the routing, the server-side rendering, the build pipeline, and increasingly the data fetching layer. You can still use React without Next.js, but if you use Next.js, you are very much living in its house. The inversion of control has crept in.

How do I evaluate a new tool to determine if it’s a library or framework?

Ask one question: “Can I use this tool for just one small part of my application without restructuring everything else?” If the answer is yes, it’s a library. You can use lodash for a single function. You can use requests for a single API call. You can use React for a single widget on a server-rendered page. If the answer is no—if using the tool requires you to adopt its project structure, its build system, its module conventions—it’s a framework. A secondary test: “Who calls the entry point?” If your code calls the tool’s entry point, it’s a library. If the tool’s runtime calls your code’s entry point, it’s a framework.

The Long-Term Bet: Modularity Over Monoculture

The software industry has a pendulum swing between frameworks and libraries. We’re currently in a framework-heavy era, driven by the desire for “batteries-included” developer experiences. But I predict a correction. As more teams get burned by framework lock-in—as they face the pain of migrating from AngularJS to Angular 2+, or from Rails monoliths to microservices—the value of library-first architecture will become undeniable. The future belongs to composable, replaceable parts, not monolithic platforms.

Your codebase is a long-term asset. Treat it like one. Don’t hand the keys to a framework unless you’re absolutely certain its architecture will outlast your business requirements. Spoiler: it won’t. Own your main loop. Call your libraries. Don’t let them call you.

— Kai Lindström