Object-oriented design gets a strange reputation in Node/TypeScript serverless circles: either dismissed as enterprise theater or over-applied into abstract factories nobody asked for. The useful version is quieter: separate decisions from dependencies, even when the deployable unit is a single Lambda.
I have refactored commerce backends where order sync, fulfillments, watermarking, and admin APIs started as “just ship it” scripts. The pattern that survives is not ceremony. It is boundaries.
The failure mode: the handler that knows everything
A typical gravity well:
- parse the event
- talk to SQS/S3/Shopify
- query Prisma
- map fields
- apply business rules
- format the response
- log something vaguely helpful
All in one file. It works until the second entry point appears (cron + queue + HTTP), or until you need to test pricing logic without AWS credentials, or until a new engineer must change one rule and accidentally breaks retries.
Boundaries that earn their keep
In TypeScript services I repeatedly return to the same layers:
- Handler / controller: translate transport into a call; translate results into status codes or queue responses.
- Application service: orchestrate use cases (
SyncOrder,ProcessFulfillment). - Domain rules / mappers: pure-ish transformations and invariants.
- Repositories / clients: Prisma, Shopify GraphQL, S3, SQS.
That is OOP as responsibility, not as inheritance trees. Classes are optional. Clear modules with intentional APIs are not.
Mixin-based Shopify clients, repository objects for Session and location maps, and dedicated mappers from upstream order shapes into GraphQL inputs are examples of boundaries paying rent: you can replace or mock one side without rewriting the story.
Why serverless makes this more important, not less
Serverless shortens deploy units and lengthens operational edge cases:
- retries duplicate side effects
- timeouts create uncertainty about what already committed
- cold starts tempt global mutable state
- observability is worse when logic is tangled with I/O
If domain rules are pure enough to unit test, you can still move fast. If they are embedded in SDK calls, every bug requires integration theater.
Refactors that actually help legacy code
When I inherit a Node/TypeScript codebase, I do not start with a framework rewrite. I start by extracting:
- input validation to schemas
- Shopify/API calls behind clients
- DB access behind repositories
- mapping into dedicated functions/classes
- side-effecting “send result” helpers away from core branching
Performance audits often uncover the same tangle: N+1 queries beside business branching beside retry logic. Modular boundaries make the hot path visible.
OOP is a communication tool
Architecture docs, ERDs, and SRS writing matter because teams cannot hold the whole system in their heads. Boundaries in code should match boundaries in conversation:
- “The mapper owns Shopify shape.”
- “The repository owns persistence.”
- “The service owns the workflow.”
When language and code disagree, onboarding cost explodes.
What I tell teams now
Use OOP ideas to keep change cheap:
- Prefer clear ownership over clever hierarchies.
- Keep handlers boring.
- Make illegal states hard to represent.
- Test the rules without the cloud.
Serverless does not remove design. It punishes the lack of it faster.
