2

I'm trying to get into using microservices, extracting functionality from a monolith into it's own (multitenant) service. However, one of the things i can't figure out how to deal with is this:

What if a microservice (triggered by an event) needs user interaction?

As an example, at the moment we have the following flow (simplified):

We have a donation module where someone can make a donation. The user enters the information (amount, payment method etc). The data gets saved in a database and a call to an external api (payment provider) will be made. The api returns a url where the user can finish the actual payment. We redirect the user to that url and when the payment has been completed, the user will be redirected back to us. We then, in case of success, activate the donation, send a confirmation email and show a message whether it succeeded or not.

Problems

There is a number of problems with this that i would like to get rid of:

  1. Tight coupling: The donation knows about the payment and the payment knows about the donation
  2. We can't easily switch payment providers because different providers need different parameters (I.E. provider a supports different payment methods than provider b)
  3. Fixing bugs is really hard because we need to roll out the fix to many different customers.

So what i think should happen is splitting this functionality into two seperate services:

When a user enters their information, an event 'donation-created' is beeing fired.

Donation service

The donation service creates the donation (of course in it's own data store etc) en then fires a 'payment-requested' event with the amount that needs to be payed. It also subscribes to 'payment-succeeded' and 'payment-failed' (and handles logic accordingly).

Payment service

The payment service subscribes to 'payment_requested', calls the external api, saves the data needed to complete the payment (I.E. url, external id etc). When the payment has been finished (let's keep the actual logic out of scope) it fires the 'payment-succeeded' or 'payment-failed' event.

This decouples the donation and payment implementations, however, because this is asynchronous, i have no idea how to redirect the user to the payment service in order to complete the process.

Solution 1

One of the solutions could be that the client is waiting for an event through long-polling or websockets but having possibly thousands of these concurrently might not be such a great idea.

Solution 2

Another one might be to forget about event based and use synchronous calls to these services but that means that the performance of the donation service is depending on the payment service which is in turn depending on the external api.

  • However you spin it your UI will have to know about the redirection mechanism. This could be done in one of three ways: 1. UI knows something about the payment service and the donation service just"patches through" between them. - 2. The donation service knows about the payment-workings and can talk to the UI about redirections and stuff - 3. The actions like "redirection" are defined in a central shared library and can be used by all services. – Falco Jul 10 '18 at 13:16
  • My answer prefers 1, since the UI will benefit from being optimized for each interactive service. – Falco Jul 10 '18 at 13:18
0

A nice way to handle usually-fast-but-sometimes-slow services is to pass a time-limit for the synchronous response as an additional parameter.

If the service can finish the whole operation in the time-limit he responds with 200 OK and the content of the response. Otherwise the service responds with 202 - Accepted and returns a URL on which the state of the transaction can be polled. This will speed up usual transactions to a single synchronous call and will gracefully handle exceptional long instances in times of pressure and provide a polling mechanism.

Polling will usually not consume too many resources, since only clients which have actively started a transaction will poll for its state and will not use up significantly more resources than synchronous processing.

And you will need a browser-refresh-compatible implementation anyways, since the client may switch networks, enter a radio-dead-zone, switch devices, refresh his browser... Which will result in a request with a new connection (possibly from a new ip address)

  • I like the idea of this, but doesn't that mean that the donation service returns the redirect url which feels to mee that it needs to know about the payment implementation? That indicates that there is coupling again. – LangeJan Jul 10 '18 at 12:46
  • The donation-service returns a donation-result which will include a payment-result which is opaque to the service. So it does not need to know about the details of a payment service, just that the interface will return some kind of payment-result, which will be sent to the client as part of the donation-result. – Falco Jul 10 '18 at 12:49
  • The payment-result can be of type payment-failed, payment-successfull or payment-pending - where payment-pending includes a callback-url to access the pending payment and maybe provide additional details. – Falco Jul 10 '18 at 12:51
  • This sound like a solid solution, think it's time to make a proof of concept. – LangeJan Jul 10 '18 at 13:05
  • You may decide if the UI code dealing with the payment-result belongs to the payment-service and include the sources there, or if your UI is a consumer which will only share the definition of the interface "payment-result" with the payment service – Falco Jul 10 '18 at 13:21
0

You don't need websockets or long polling for this.

Payment service:

  • CreatePayment(paymentDetails) returns Payment Id and the redirect Url
  • CompletePayment(string paymentId, string confirmationCode) check the confirmation code with the 3rd party payment provider and mark payment complete. return the result

Subscribe to/publish events (CreatePayment, PaymentmentCreated, CompletePayment, PaymentCompleted) or call direct

  • I don't completely understand how this would integrate in the donation process. Does it mean that the CreateDonation(donationDetails) calls the payment service CreatePayment(paymentDetails) and returns the redirectUrl or that the client (which usally will be a webbrowser) calls the CreatePayment based on what de CreateDonation returns? – LangeJan Jul 10 '18 at 12:43
0

Three pieces of advice:

  1. Tease apart front end from back end. Microservices are just better back end database interfaces. Don't conflate with front end interaction.
  2. Keep orchestration firmly in the front end/user interaction. Back end components should be completely orchestration agnostic.
  3. Don't use cross-system events for user interaction/orchestration. Events are subject to latency and are not reliable. Latency and unreliability make for very poor user interfaces.
  • Thanks for the advice, i don't completely understand wat you're saying: 1) agreed, but backend services shouldn't make it impossible to be glued to another service? 2) You mean that the client in the example should call the donation server first en then the payment service? 3) What do you mean with cross-system? No events between services or no events between services and clients? – LangeJan Jul 10 '18 at 13:31
  • The short version is- you want 3 distinct things- a back end donation CRUD REST service, a back end payment CRUD REST service wrapping the particular 3rd party API, and the sequence of operations- first do this, then do that- lives in client side code, using synchronous calls to services. – Jonah Benton Jul 10 '18 at 13:46
  • Backend services instantiate/represent/enshrine the domain data model. In the domain, Donation and Payment are tangibly different things. The one does have to know about the other- a payment has to be associated with a donation, and the promise of a donation is only kept with a payment. This is done by having a key to a donation in the payment data model, and a key to a payment in the donation data model. The services don't depend on or know about each other but a client/orchestrator does. So use data/keys to glue, via orchestrator clients. Don't do it directly in the services. – Jonah Benton Jul 10 '18 at 14:48
  • Yes, client orchestrator orchestrates the flow between services. Orchestration is a first class entity in any system where sequencing matters. – Jonah Benton Jul 10 '18 at 14:50
  • Ok to use events as an optimization. Raw events will get lost. System should not fail if an event is lost. – Jonah Benton Jul 10 '18 at 14:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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