4

When would you consider sharing a database to be fine? What are your rules?

Our team in-charge of in-store credits.

We:

  1. Calculate how much credit the customer earned
  2. Keep track of how much credit the customer earned and spent. Think of a bank but for in-store credits instead of money.
  3. Expire unspent credit. Think of gift cards that has an expiry date.
  4. Send an email to the customer when they have credit that are about to expire. Emails are only sent once a month.

We decided to implement these functionality in two separate components.

  1. A REST API that calculates and keeps track of the credit that each customer has.
  2. A background worker that sends the email notifications

How can the background worker know which customer has credit that are about to expire?

The options that we though of are:

  1. The background worker subscribes to "credit" domain events emitted by the REST API and stores the consumer' credit in its own database.
  2. Expose an internal API operation in the REST API which the background worker will call.
  3. Both the background worker and REST API queries the database directly. We will introduce a shared persistence library that both the API and background worker will use.

I know that the usual advice is either option 1 or 2, but when would you consider option 3?

3 Answers 3

8

Your "components" should probably be part of the same "service", even if they are run as different executables. If this is the case it should be fine if both access the same database.

My reasoning:

  1. The domain is the same (i.e. in store credits)
  2. There is one team
  3. They are presumably deployed at the same time

The point of microservices is to ensure components are independent. But this is as much, or more, of an organizational pattern than an architectural pattern. The goal is to allow teams to work undisturbed, without minimal dependencies between teams. See also Conways Law.

If this is part of your teams responsibilities you can implement the functionality however you want. So either option is just fine. You can divide your functionality into internal projects, sub-modules, executables, services or whatever you want to call them. As long as you maintain a API towards the other teams with whatever service guarantees you have agreed upon.

You could of course decide the email functionality should be a completely independent service. But then you should probably create a separate team for this, centralize all email and message handling to this one service, agree on an API etc.

1
  • I wholeheartedly agree with your answer. I currently prefer these executables to have independent deployments (and I only need to deploy one thing when changing the persistence layer), but it's worth questioning my preference. Commented Nov 21, 2023 at 19:39
2

Working through your options:

  1. The background worker subscribes to "credit" domain events emitted by the REST API and stores the consumer' credit in its own database.

My primary concern for this option would be DRY. I suspect that in the use case you describe there will be a subset of both the database schema and program logic which is basically identical between the API and the background worker.

When you do maintenance you may need to make a code change, a database change or both. Assuming that you don't use some elements of option #3 - i.e. you keep the API and worker services completely separate (separate DB and separate code base) you would have to do double work for each change that affected the shared logic.

The counter point to this, would be if the worker has radically different logic/schema (almost nothing shared), in such a case it may make sense to have two services.

  1. Expose an internal API operation in the REST API which the background worker will call.

This is a good option from a code structure/maintenance perspective. You can keep all the detailed calculation logic together. The external component would only need to deal with the concerns of scheduling a batch job (once per month) and the actual email sending functionality.

If the batch is particularly large you may want to stream the data rather than the typical solution (of building a large JSON document in memory then throwing it over the wire). To be clear you could structure your code to stream the results over the network, but it may increase the complexity of your code.

Another concern is monitoring, when the job runs it may create a noticeable spike in DB load (this will happen regardless of the option you choose), however handling the "internal" request may throw off other metrics - so you may need to customize your metrics to ensure you don't get an alert once per month.

  1. Both the background worker and REST API queries the database directly. We will introduce a shared persistence library that both the API and background worker will use.

Several common frameworks provide the ability to start up as either an "API head" or as a background worker, based on command line arguments. This is an alternative to creating three projects (the shared library and two applications).

In my experience it creates a lot of work to initially separate the project into three and then to do ongoing maintenance. Instead, I typically just package the entire code base once (using something like Docker) then launch the application as either an API head or background worker as required.

1

The main problems with sharing a database is that if the database ever requires refactoring, it is much more difficult to perform refactoring on a database than it is to perform refactoring on an API.

For example, there is no such thing as a revisioned database table. This means that when (it will happen, given enough time) you create a column that is missing a NOT NULL clause, you might be able to add it, but you will risk breaking the "database API". How could adding a data value break the API? The service writing into it might not honor the NOT NULL for every entry.

Likewise, if you decide a field in a column needs refactored into a joining table, the coordination between services that are communicating with each other "through the database" can lead to scenarios where it is difficult to coordinate.

When sharing data, use the network. It permits two or more requests for the same data, by the service providing it handling two or more API versions (/v1/customer and /v2/customer). It permits two or more responses even when the service isn't cooperating, because your client code can adapt the /v2/customer response into a /v1/customer data structure.

Your Answer

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

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