-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Prerequisites
- I have searched the existing issues to avoid duplicates
- I understand that this is just a suggestion and might not be implemented
Problem Statement
Our backend services are becoming tightly coupled to specific third-party SDKs (e.g., SendGrid for email, Stripe for payments, Auth0 for auth). This direct coupling creates several problems:
- High Refactoring Cost: Switching from one provider (like SendGrid) to another (like Mailgun) requires finding and rewriting all implementation-specific code, which is brittle and time-consuming.
- Difficult Testing: Our service-layer tests (e.g., in our Express handlers) become complex as they need to mock specific, and often stateful, third-party libraries.
- Lack of Flexibility: It's hard to support multiple providers (e.g., Stripe and PayPal) or add fallbacks without cluttering our core business logic with conditional statements.
Proposed Solution
Introduce a Functional Adapter Pattern to abstract these external services. This approach avoids heavy OOP classes and fits well within a functional JavaScript/Express paradigm.
The solution involves three main steps:
- Define Standard Interfaces: For each service type, we define a simple, stateless, functional "interface" (i.e., an agreed-upon function signature and data shape).
- Email: sendEmail({ to, from, subject, body })
- Payments: createCharge({ amount, currency, sourceToken })
- Auth: verifyToken(token)
- Create Adapter Modules: For each provider, we create a specific adapter module that implements this standard interface. This module is the only place that knows about the third-party SDK.
- src/adapters/email/sendgrid.adapter.js (exports sendEmail)
- src/adapters/email/mailgun.adapter.js (exports sendEmail)
- src/adapters/payment/stripe.adapter.js (exports createCharge)
- src/adapters/auth/auth0.adapter.js (exports verifyToken)
- Create Service Entrypoints: We create a central service file (e.g., src/services/email.js) that uses a factory or environment variable (e.g., EMAIL_PROVIDER) to select and export the functions from the active adapter.
Example (src/services/email.js):
import * as sendgridAdapter from '../adapters/email/sendgrid.adapter.js';
import * as mailgunAdapter from '../adapters/email/mailgun.adapter.js';
const providers = {
sendgrid: sendgridAdapter,
mailgun: mailgunAdapter,
};
// Select the active provider based on config
const activeProvider = providers[process.env.EMAIL_PROVIDER] || providers.sendgrid;
// Export the adapter's function under the standard name
export const sendEmail = activeProvider.sendEmail;- Refactor Core Logic: Finally, we update all Express handlers and business-logic services to import from the central service entrypoints (src/services/email.js) instead of directly from an SDK.
This decouples our business logic from the implementation details, allowing us to swap providers just by changing an environment variable.
Alternatives Considered
-
Status Quo: Continue with tight coupling. This is not viable for a scalable SaaS, as it leads to technical debt and slow development when changes are needed.
-
OOP/Class-based Adapters: We could use class EmailService with a TypeScript interface. However, given our functional JavaScript approach, simple function modules are more lightweight, composable, and easier to test without dealing with this context or class instantiation.
-
Third-Party Abstraction Libraries: We could use a library that already abstracts multiple email providers (e.g., nodemailer). This adds another dependency to maintain and may not cover all our specific use cases or future provider choices for payments and auth.
Additional Context
This is a purely architectural refactor that will significantly improve testability and long-term maintainability.
Testing Benefit (Example):
-
Before: jest.mock('@sendgrid/mail', () => ({ send: jest.fn() })); (Test needs to know about SendGrid).
-
After:
// We can easily mock our own service module
import * as emailService from 'src/services/email';
jest.spyOn(emailService, 'sendEmail').mockResolvedValue({ success: true });Priority
Medium