I was pairing with a backend engineer last summer on a permissions middleware in Python. They had stacked four decorators on a view function, hit a permissions error that should have been caught two layers up, and stared at the traceback for five minutes before asking me, "why does the @logged decorator see the raw request and not the authenticated one". The answer is that the wrap order of @-decorators is bottom-up, and I want to use that as the way in to a topic that confuses people more than it should: the Decorator pattern from Design Patterns: Elements of Reusable Object-Oriented Software (the GoF book) and the language feature called "decorators" in Python, TypeScript, and friends share a name and almost nothing else.
My stance for this article: they are different things, the OO Decorator pattern is rarer than language decorators in modern code, and most of the bugs people attribute to "the decorator pattern being weird" are really bugs about wrap order or about confusing one with the other. I will show you both, the cases each one fits, and the rule for the wrap order that I write on the whiteboard every time I onboard a new engineer.
The OO Decorator pattern, in one screen
The original pattern is structural. You have an object that conforms to an interface, and you want to add behavior to it without subclassing the concrete class. You wrap the object in another object that conforms to the same interface and forwards calls to the inner object, adding behavior before and/or after the forward.
Three properties of that snippet that define the pattern. The wrappers and the wrapped class share the same interface, which means a caller cannot tell whether they are talking to a plain EmailNotifier or a wrapped one. The wrappers compose: I can wrap a wrapped notifier as many times as I want. The decision of which decorations apply happens at construction time, not in the class definition. That last property is the actual point. You are configuring behavior with composition, not committing to it with subclasses.
The pattern is rare in modern code for two reasons. First, in any language with first-class functions, this is more naturally expressed as function composition: withTiming(withRetry(emailNotifier.send)) is the same idea with less ceremony. Second, the few cases that genuinely benefit from the OO form (multiple methods that all need wrapping, stateful wrappers that need to share state across calls) are usually better served by middleware lists in modern frameworks. I have shipped the OO Decorator maybe twice in eight years.
Language decorators are syntactic sugar over function transformation
A Python or TypeScript @decorator looks similar at first glance, but the semantics are different. @logged def foo(): ... is shorthand for foo = logged(foo). The decorator is a function (or a callable class) that takes the original function and returns a replacement. There is no shared interface, no instance, no composition of wrappers around an inner reference. There is a single name in the local scope, and applying a decorator rebinds that name to whatever the decorator returns.
The language decorator is closer to a higher-order function than to the GoF pattern. The wrapper is just a function. The signature does not have to match (though it usually does, by convention). There is no "shared interface" because Python is duck-typed and TypeScript decorators (the new Stage 3 ones) are a metadata-and-replace mechanism with their own quirks.
This is why the same word covers both ideas: both "decorate" an existing thing by wrapping it. But the OO pattern is about object composition under a shared interface, and the language feature is about function transformation under a shared callable contract.
The wrap order rule that catches everyone
This is the bug my co-worker hit. The textbook way to state the rule is: language decorators apply bottom-up at definition time and call top-down at call time. Both halves of that sentence matter, and they are easier to keep straight if you remember @a @b def f rewrites to f = a(b(f)).
When you call get_profile(req):
auth_required's wrapper runs first. It sees the raw request. If it does its work before delegating, it can short-circuit on a missing token without involving the other layers.auth_requiredcalls the next inner wrapper, which islogged's wrapper. By the timeloggedruns,auth_requiredhas already done its checks (and possibly attached an authenticated user object).loggedcallsrate_limited's wrapper. By the timerate_limitedruns, both auth and logging have already happened.rate_limitedcalls the originalget_profile. That is the innermost call.
My co-worker had @logged ABOVE @auth_required and was confused why logged saw the raw request: because in that arrangement, logged IS the outermost wrapper and runs before auth_required has done anything. The fix was a one-line reorder.
The rule on my whiteboard for new engineers: "the topmost decorator runs first when the function is called. The bottommost decorator is closest to the function and runs last." Some people remember it as "top is outer, bottom is inner". Whatever phrasing sticks; the important thing is that you know the rule before you stack four of them.
This rule is also why the order of decorators is a real semantic choice in real codebases. @cache @retry means cache the result of the retried call (one cache miss can cause many retries before the cache fills). @retry @cache means retry the cached call (a transient cache failure can be retried). Those are not the same behavior. I have shipped the wrong one in a hurry.
The OO pattern handles wrap order differently
In the OO Decorator, the order is set by the order of construction:
Reading left-to-right, the outermost wrapper (the one whose method runs first) is TimingDecorator. The innermost wrapped object is EmailNotifier. The convention is the reverse of language decorators visually: with @-syntax you list outer-first top-to-bottom; with constructor calls you wrap outer-first left-to-right. Either is fine in isolation, but mixing them in the same codebase, or worse, in the same file, is a recipe for confusion. I push for one or the other in any given module.
A real bug: a decorator that loses the function signature
A failure mode I hit early on, and that I still see in code reviews. A naive Python decorator looks like this:
It works for runtime calls, but it silently mangles three things people depend on: wrapped.__name__ is now 'wrapper', wrapped.__doc__ is gone, and the function's signature (visible to inspect.signature) is now (*args, **kwargs) instead of the original parameter list. Tools like FastAPI, Click, pytest, and most introspection libraries read those, and a bare decorator breaks them in subtle ways. The fix is two characters of Python:
@wraps(fn) copies __name__, __doc__, __wrapped__ (which lets inspect.signature find the original), and a few other attributes. The TypeScript story is messier because the new Stage 3 decorator API does not have a single equivalent helper, but the principle is the same: a decorator that does not preserve the metadata of what it wraps is going to bite you the first time a framework tries to introspect the function. I treat "bare decorator without @wraps" the same way I treat "variable shadowing in a callback": a warning sign that the author had not finished thinking about the implications.
Where the OO Decorator still earns its keep
Three cases I still reach for it:
- Multiple methods need consistent wrapping. If your
Notifierhadsend,cancel, andsubscribe, and aLoggingDecoratorshould log every call to every method, the class form keeps the consistency obvious. A bag of higher-order functions does the same job with worse readability. - Stateful wrappers that share state across calls. A
RateLimitDecoratorhas a counter and a window. Putting the state in a class is more natural than threading it through closures. - Plugin-style architectures where wrappers are user-supplied. If you publish an interface and let consumers ship decorators, the OO form is a stable contract. Function-composition is fine in your own codebase but harder to expose as a public API.
I explicitly do NOT use the OO Decorator for one-off cross-cutting concerns. For "add timing to this single function", a higher-order function is a one-liner. The classy version is overkill until you have at least two methods or two wrappers.
Where language decorators get misused
The most common misuse I see: putting business logic in a decorator. A @bill_for_usage decorator that not only logs the call but also writes a row to the billing database is a maintenance trap, because the side effect is invisible from the function's signature and the function's tests. Decorators should be cross-cutting (logging, timing, auth, retry, caching, schema validation). The moment a decorator touches domain state, refactor it back into a normal function call inside the function body.
The second misuse, specific to TypeScript: using class decorators (the new Stage 3 ones) to inject behavior into instances. The decorator API gives you access to the constructor and the prototype, but the typings are still rough and the IDE support varies by framework. I use them only when a framework (NestJS, TypeORM) demands them. For my own code, I prefer factory functions and dependency injection.
A decision tree, finally
When I have to add wrapping behavior, the question I ask in this order:
That tree covers most of the cases I have hit. The only one that breaks the tree is when you need a public plugin API, which is the case where the OO pattern's interface contract genuinely earns its weight.
A thing worth memorizing
If you remember nothing else from this article, remember this rewrite. @a @b def f is f = a(b(f)). The topmost decorator IS the outermost call. The bottommost decorator is the closest to the original function. Half the bugs around stacked decorators come from getting that backwards in your head. Drawing it on a sticky note next to your monitor is genuinely a good investment for the first month of working in a codebase that uses them.
