Code & DevelopmentCode & Development · 23 Aug 2026
Framework vs SDK: Who Calls Whom
Most developers memorize names, but few understand the mechanism. Here is the simple rule to tell a framework from an SDK and level up your debugging.
Every week someone asks me some version of this:
Is Express a framework or a library?
Is the OpenAI SDK a framework?
Should I learn frameworks or SDKs first?
It sounds like a beginner question, but the confusion sticks around well into year three of a career. And it is not for lack of studying — it is that almost nobody explains the mechanism behind either one. People memorize names instead.
Once you understand the mechanism, something useful happens: you read docs faster, debug with less guesswork, and pick tools without depending on a Twitter thread. Let us get into it.
The starting point: who calls whom

Hold on to this sentence, because it settles about 80% of the confusion:
You call the library. The framework calls you. The SDK talks to the outside world.
It sounds reductive. It is not. It is literally about your program''s flow of control — who is holding the steering wheel during execution.
Library: you are in charge

A library is a bag of ready-made functions. You import it, call it when you feel like it, and execution comes right back to you.
import { format } from 'date-fns';
const today = format(new Date(), 'MM/dd/yyyy');
console.log(today); // your code is still drivingfrom datetime import datetime
today = datetime.now().strftime('%m/%d/%Y')
print(today) # your code is still drivingYou are the director. The library is the actor who steps in when you yell action. If you never call it, it does nothing.
Framework: it is in charge (and that is the whole point)

Here everything flips. With a framework, you do not call its code — it calls yours.
This has a name: Inversion of Control (IoC), also known as the Hollywood Principle: do not call us, we will call you.
import express from 'express';
const app = express();
// You are NOT executing anything here.
// You are REGISTERING a function and saying:
// "when a GET hits /users, call this"
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Ana' }]);
});
app.listen(3000); // from this line on, the framework is in controlfrom fastapi import FastAPI
app = FastAPI()
# Same idea: the decorator registers your function with the framework.
# It decides when the function runs, not you.
@app.get("/users")
def list_users():
return [{"id": 1, "name": "Ana"}]Notice what you did not write in either snippet:
you did not open a TCP socket
you did not parse raw HTTP text
you did not build the response headers
you did not write the infinite loop waiting for connections
you did not handle concurrent requests
All of that exists and is running right now. It just lives inside the framework.
What a framework really does under the hood

Every framework — web, UI, testing, whatever — runs on the same three-beat skeleton.
1. Registration phase. You declare your pieces: routes, components, middleware, jobs, test cases. The framework stores them in an internal structure. app.get(...) does not execute your function; it pushes a record onto a list.
2. Bootstrap phase. You hand over control (app.listen(), uvicorn main:app, npm test). The framework assembles what it needs: server, dependency injection, connections, component tree.
3. Execution loop. The framework runs forever, listening for events. When one arrives, it consults that list from step 1, finds your function, and calls it — handing you pre-chewed data (req, res, props, event).
Here is crude pseudocode of what Express is doing while you sleep:
// this is the framework, not your code
while (true) {
const rawRequest = await socket.acceptConnection();
const req = parseHttp(rawRequest); // becomes an object
const route = routingTable.find(req.method, req.url);
if (!route) return respond404();
for (const middleware of middlewares) { // lifecycle
await middleware(req, res);
}
await route.handler(req, res); // <- YOUR code, at last
}This is why frameworks have a lifecycle (useEffect, beforeEach, middleware, onMount). Those are hooks the framework offers so you can inject code at specific moments of the loop it controls. You do not get to choose when your code runs — only where it hangs.
It is also why frameworks are opinionated. By letting one drive, you accept its folder structure, its naming, its way of doing things. In exchange you get speed, and you do not rewrite HTTP parsing for the thousandth time in the history of computing.
SDK: the translator between your code and someone else''s system

SDK stands for Software Development Kit. In practice, today, nearly every SDK is the same thing: a package that wraps a service''s API so you do not have to speak raw HTTP.
Without an SDK, integrating with a service looks like this:
import uuid
import requests
response = requests.post(
"https://api.example.com/v1/charges",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"amount": 5000, "currency": "usd"},
timeout=30,
)
if response.status_code == 429:
... # now what? backoff? how many retries?
if response.status_code >= 500:
... # retry? did the charge already go through?
data = response.json() # untyped dict, no autocompleteWith an SDK:
from stripe import StripeClient
client = StripeClient(api_key=api_key)
charge = client.payment_intents.create({
"amount": 5000,
"currency": "usd",
})
print(charge.id)import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const charge = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
});
console.log(charge.id);Three header lines and all the error handling vanished. But they did not stop existing — they just moved.
What a serious SDK handles for you

Authentication. Builds the header, refreshes expired tokens, reads the environment variable.
Serialization. Turns objects into JSON and JSON back into typed objects. You get autocomplete, which is underrated until you lose it.
Retry with exponential backoff. Got a 429 or a 503? Once you enable it, the client retries quickly on the first failure and then on an exponential backoff schedule, instead of giving up. Note it is opt-in: Stripe ships the mechanism, you configure it.
Idempotency. Generates a unique key so a retry does not charge your customer twice — Stripe stores that key for 24 hours and replays the original result. This one item alone pays for the SDK.
Pagination. Turns cursor juggling into a plain iterator.
Typed errors. RateLimitError, AuthenticationError — instead of comparing loose status numbers.
Versioning. Pins the remote API version so the server does not shift under your feet.
None of this is magic. It is boring, tedious, edge-case-riddled code that somebody already wrote, battle-tested in production, and maintains for you. That is the SDK.
The table worth taping to your monitor

Trait | Library | Framework | SDK |
|---|---|---|---|
Who calls whom | you call it | it calls you | you call it |
Flow of control | yours | its | yours |
Problem it solves | one specific task | your app structure | talking to an external system |
Where the work runs | your machine | your machine | someone else''s server |
How many per project | dozens | 1 (rarely 2) | one per service |
Cost of switching | low | brutal (rewrite) | medium |
Examples | lodash, date-fns, pandas | React, Django, Rails, Spring | Stripe, AWS boto3, OpenAI |
The most practical row is the cost of switching. Swapping date-fns for day.js is an afternoon. Swapping Django for FastAPI is a quarter. That is why framework choices deserve a meeting and library choices do not.
In real life, all three live in the same file

And there is nothing wrong with that:
import os
from fastapi import FastAPI # framework: runs the application
from stripe import StripeClient # SDK: talks to Stripe
from datetime import datetime # library: one specific task
app = FastAPI()
stripe = StripeClient(api_key=os.environ["STRIPE_KEY"])
@app.post("/subscribe") # the FRAMEWORK will call this
def subscribe(amount: int):
charge = stripe.payment_intents.create({ # YOU call the SDK
"amount": amount,
"currency": "usd",
})
return {
"id": charge.id,
"created_at": datetime.now().isoformat(), # YOU call the library
}Three import lines, three different roles. The framework is the stage. The SDK is the phone. The library is the tool on the workbench.
What changes in your head after this

You debug better. A bug in framework code is almost never the framework''s fault — it is you using a hook at the wrong moment of the lifecycle. A bug in SDK code is almost always network, credentials, or rate limits. Knowing that cuts your investigation time in half.
You read docs in the right order. Framework docs: start with the lifecycle and the folder structure. SDK docs: start with auth and the error codes. Library docs: go straight to the function signature.
You choose with actual criteria. A framework is a marriage — check the community, the maintenance, the release cadence. An SDK is a vendor — prefer the official one, verify it has retries built in, and see how long since the last commit.
The rule of thumb

Facing any unfamiliar package, ask one question:
Does my code call this, or will this call my code?
The three answers:
You call it and the work happens on your machine — library.
You call it and the work happens on another company''s server — SDK.
It calls you and dictates your project structure — framework.
That is it. Nothing else to memorize.
If this was useful, the natural next step is to open the source of whatever framework you use most and go find the main loop. Seeing with your own eyes the exact moment it calls your function is one of those things you cannot unsee — and it is what separates people who use tools from people who understand them.
Sources
Inversion of Control — Martin Fowler — the canonical definition. Fowler puts it plainly: the control is inverted, it calls me rather than me calling the framework. Also where the Hollywood Principle is tied to the concept.
Idempotent requests — Stripe API Reference — official documentation on idempotency keys: V4 UUIDs recommended, up to 255 characters, pruned after 24 hours.
Advanced error handling — Stripe Documentation — how the official libraries retry: first attempt quickly, then exponential backoff, respecting the Stripe-Should-Retry header. Confirms retries are opt-in and must be configured.
The framework definition Fowler cites comes from Ralph Johnson and Brian Foote: the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user''s application code. That sentence is from 1988 — the idea in this article is older than most of the tools it uses as examples.