1
\$\begingroup\$

I am writing a small micro-service in typescript with NestJS. This microservice is responsible of giving a user access to one of our company's systems trial access.

At first I used hubspot integration to add new contacts and send emails, but then I ended up using SendGrid. Both of these services can do almost the same thing (create and add contacts to different lists, send emails automatically, API to send mails and such).

So I decided to write something abstractly, which can be helpful if one day I or someone else decides to move from sendgrid back to hubspot or even write a new implementation of another similar service.

So let's get to the problem

I have created an interface which is called TrialUserManager, this interface will contain methods on the high-level of what should be done:

export interface TrialUserManager<T> {
    createAccessListContactRequest(userInformation: RegisterRequestDto): Promise<any> | any;
    sendEmail(data: T);
}

So right now, there are two functions:

createAccessListContactRequest - Should create a new contact in a specific list. A list is just a collection of contacts (users) in Sendgrid, which is also a feature in Hubspot. So all it does is adding the given user information to that list using the API of Sendgrid or other based on the implementation. RegisterRequestDto is something globally shared in the microservice, so everyone should know about it, and all services (sendgrid, or hubspot) should have no issue to use it because it's mainly key-value data of the user like first_name, last_name, email, etc.

sendEmail - Basically just sends an email using the API of the implementation.

Now the issue is sendEmail, because the excepted function input is different for Sendgrid and for Hubspot, for instance, in Sendgrid you must send a templateId while in Hubspot you don't, it's some other inputs.

So that's the reason of the generic type T in the interface, which represents the email options input.

So that's how it's used:

First of all, for these who don't know NestJS, I assign the instance of the service in the right module, to provide either SendgridService or HubspotService, both implements TrialUserManager like this:

const getTrialManagerServiceByProviderType = (type: TrialManagerProviders) => {
    switch(type) {
        case TrialManagerProviders.HUBSPOT:
            return HubspotService;
        case TrialManagerProviders.SENDGRID:
            return SendgridService;
        default:
            return undefined;
    }
}

And in the Module providers I use it as this:

providers: [
    {
        provide: 'TrialUserManager',
        useClass: getTrialManagerServiceByProviderType(TRIAL_MANAGER_SERVICE)
    }
],

So now the injectable string TrialUserManager will provide the right instance, TRIAL_MANAGER_SERVICE is a global static variable that is assigned to Sendgrid.

So back to the implementation usage, I inject TrialUserManager into the place that it's going to be used, in the constructor:

@Inject('TrialUserManager') private readonly trialUserManager: TrialUserManager<SendgridEmailOptions>,

SendgridEmailOptions is the object, dedicated to Sendgrid only, that I pass as a generic type to TrialUserManager.

But I have a feeling that I am over-complicating it and that this is not right to do, because anyone that uses TrialUserManager should never care what data type you should pass to it, it's an high level interface and you should not change the types every time you switch the service (sendgrid or hubspot).

I would love to get an advice, best practice about this because this will also help me with other daily problems.

I hope I elaborated properly, and put in enough information and thanks in advance!

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I thought you were writing microservices, but what you're actually doing is writing a monolothic application that just happens to be distributed across a network.

Let me rephrase this a bit: One of the core ideas of microservices is to allow you to switch out the implementation of a service with another implementation at runtime, so long as these implementations perform to the specifications of your business.

As you have written this, the lookup created here disallows exactly that, because the lookup binds to a specific implementation of the service you're attempting to access...
Instead of all of these shenanigans, what you want is a mediator between services and a common "global" data format that is used to communicate. A service that uses the TrialUserManager should just have to pass data conforming to a TrialUserOptions interface that is understood by both implementations. Otherwise you can never phase out the SendgridTrialManagerProvider without checking all of your services for any usages.

This whole thing seems like you really need to get back to the drawing board on the idea of microservices and deconstruct your application into even smaller service units and to add some mediator or message queue to abstract the communication away from you.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.