Microservices: Should a database only be accessible via API, or should all services in the domain have direct access?

I have a fairly standard microservices architecture. RESTful APIs and Workers, all connected through queues and message passing.

I have a users CRUD API that can support all user database operations. If I have a worker in the same domain that's listening to events and will need to change values in the user database based on the events. Should that user worker access the user database directly (through a seperate connection string), or should it use the user API?

Using the API would mean that the worker and the API are tightly coupled, and changes that would affect the worker, could also require a change to the API. This introduces coupling and increases the cost of change. On the other hand, the worker having direct database access means there isn't a single interface for the data, but would allow these to be deployed independently (minus breaking changes).

I feel like both are valid approaches, but I'm lacking the right terms to Google, and would love any suggestions or resources that have helped you decide between the two. Thanks!

Posted Mar 4, 2020, 4:04 PM UTC

2823
enchantedforrest•

We have had many issues with some people doing direct db updates instead of using api and this causes a lot of hard to debug problems downstream. I know this is a little different situation, but having one interface to the data seems to work better for us.

16
RaidionOP•

Can you go into some of the details what those are? Are those issues model drift where you have an string value in the model of "Thing" in one worker and "thing" in the other worker, and during processing, you do a if(x == "thing") and you get a bug?

3
microbial64•

All data access to a specific datastore should go in one service.

Think of what you are planning, if you have to impose or relax a constraint on the user data model (say, adding a company or tenant id), now you might have to update two places of logic and deploy two services. This becomes a distributed monolith (at least for the user model).

7
RaidionOP•

Couldn't you apply the same argument to the inverse though? If we add a new constraint, we would always have to update the API AND We would have to update the worker to pass in the new constraint when making the API calls, even if the worker really didn't care. You now have to update two places of logic and deploy two services.

If your API now needs a company/tenantId to validate permissions, your worker would either need to have those to pass into the API, OR it would need a role that would bypass those constraints anyway. That's a tighter coupling than just needing the UserId.

6
microbial64•

That would be an unstable API. "Public" APIs (public here being those exposed for external use to the service itself, not the public at large). APIs should be stable and if you introduce a breaking change you should release a new version (2.0 if you will).

Then it's a transitional problem of when to deprecate the old API. You still have to solve the required data problem on 1.0 but, using our example of of tenant id, the reason you added that might be to support more tenants while still running your current tenant. Doing so in this way allows you to update the other apps as they need to support new tenants.

The problem of a company/architecture/department wanting "all services updated to support multiple tenants" is a business problem. Microservice architecture allows you to update these as independent concerns which is a technical domain and orchestration technique.

6
RaidionOP•

I'm gonna reply to you again because I have another thought and this is a good discussion.

If my worker has direct DB access, there are 3 things that can take it down: messaging queue failure, database failure, and the container/cluster failing.

If the worker has API access, there are many more failure methods: DNS, load balancer, authentication, authorization, API, database, messaging queue, or either of the containers failing.

Lots more dependencies.

4
microbial64•

I'll reply to both your responses here. I like the discussion so this is great to me.

I feel at odds with a couple of phrases so I'll first point that out and then go on, if I'm wrong then ignore the second part ;).
"... you have to add that value to the API" you say "not a breaking change" but then say "then change the worker to call the API". Those seem slightly at odds with each other but to make a non-breaking change (call it 1.0.1" that now has the ability to accept a new field "x". Then it has to accept interactions that don't contain "x". Meaning you have to either default it or make a best-guess effort.

​

The first point here is to have independently deployable services. If your worker API needs a piece of functionality and that belongs in the User API, then it's a change request to the User API. That is fine as a feature dependency. You make that change and then deploy it. It is its own independent work. Then in the worker, which might be the first of many to need that feature, it will interact with the new API. This means the other 300 microservices could go on none the wiser that either the worker or User API updated.

​

You could (technically) just connect to the DB and insert data without the new field "x". However, as software moves what about the logic to default "x", or some day making it required. Now you either have to have inconsistent/incomplete data or several places you have to insert that logic. You could make that a user library that takes care of this as an alternative but then we're not talking microservices right? Plus, now you have an unknown service inserting (change data capture is slightly harder than side-carting your one and only User API).

What happens if the user model now needs a graphing database to express complex relationships. Now you'd have to update all the code that directly accesses the DB for users just to operationally migrate to this. Even with the library approach, maybe it takes new settings to connect to that DB so you'd have to update the library as well. This is the second point, locus of control.

6
ivix•

You can ignore the advice you've been given, or not. It's up to you.

0
RaidionOP•

Yes, you don't want to go about breaking API contracts, that's a API 101 lesson.

Lets say you want to add a property on the user, that represents a processing state that's not used on the frontend. If you have only an API connection, you have to add that value to the API (not a breaking change) and then change the worker to call the API. This is a dependency. If your worker had a direct database connection, it would be able to update and read that state without any changes to the API at all, and would make your user worker microservice much more independently deployable.

2
[deleted]•

[removed]

1
ivix•

A database must only be accessed by a single application. This is pretty much a golden rule.

If the worker is part of the same application then it may have direct database access.

A "single application" can be defined as a system deployed from the same repository at the same revision.

5
RaidionOP•

That's what everyone "says", but I do feel like it's cargo culting to an extent. There are obviously trade offs to both approaches, and I don't feel like I've seen an article discussion those tradeoffs in depth.

An obvious counterpoint to "must be accessed by a single application" is performance. Adding an api is a huge amount of latency in some situations. Does every high performance application only access a database through an API? If not, then does it only access it asynchronously?

2
ivix•

If your application requires data from another application at very low latency, I'd say it wasn't well designed.

It's not cargo culting. It comes from years of experience. If you had that you would know that multiple applications developed by multiple people accessing the same database will not end well. To make it even slightly work you need a DBA enforcing consistency through stored procedures.

https://microservices.io/patterns/data/database-per-service.html

5
remain_calm•

One database per service. It's a question of encapsulation. One database for all services is similar to using global variables for state. It seems way more efficient in the beginning but quickly turns into a nightmare to maintain and debug.

4
sth2258•

Chris Richardson, who is basically the father of microservices would consider shared database as an anti-pattern. Instead he would suggest One DB per Service with transactions being constructed over Saga. More about those patterns here: https://microservices.io/patterns/data/database-per-service.html

If you were to implement shared, I would strongly advise looking deeper into your usage of microservices as an architecture and seeing if package-dependant monoliths might be better.

4
andy9775•

Through the service.

If you have validations set up you’ll have to duplicate that. Logging - same thing. Eventually the other service will want to change the dB and you’ll end up with a distributed monolith where each service does its own thing. Debugging will require you to touch both services.

3
cutecoder•

Encapsulation says you need to have a single interface to data and work only through that interface.

However if performance needs dictates, you can also fulfill that by having stored procedures to encapsulate all tables, manipulate data strictly through those stored procedures, then make all tables inaccessible to any application that connects to the database (via permissions or the like). Hence the set of stored procedures essentially becomes the API to the data, and you can evolve how the data stored independent of its interface.

Therefore you don't need your database to be bogged down by performance limitations of REST API yet achieve full encapsulation.

3
h4xrk1m•

I've seen this shared database "pattern" a number of times, and it's always tremendously complicated. It will take you down the path of 3 AM debug sessions and change requests becoming impossibly difficult to implement.

The better alternative is to give each service its own database, and exposing data via API if and when it's necessary.

3
Java_Beans•

Putting all the tech terms aside, I will approach this using a different analogy.

Imagine you are running a restaurant and you figured to run things smoothly, you dedicated one (or group) of people for each function.

One of the important resources you have is the fridge. The fridge is your biggest and most important items resource. You need people to replenish, keep checking what's missing or going down, and you need other people who can take out of it to serve the cooks.

How will you manage this resource (database of items), will you put a team for buying items and adding to it (write) and another team to pull from it (read)? ASSUMING you will be 100% reliable in not having the replenish team remove items and not having the fetch team add items, you will still have other issues for example, what if the replenishment team came in one day and decided to reorder all the shelves and put the older items in the front? The fetch team the next day will be totally lost and the restaurant will come to a halt until they figure it out.

So you decide, ok let's make it one guy (team) to manage the fridge in/out, the immediate next problem is, but this guy have nothing to do with operations/logistics to order new items he can barely focus on organizing the fridge and fetching from it. Who will handle buying/replenishment then? You dedicated another person with the sole job of going to the market and buying, this person has nothing to do with fridge organization, he won't even get access to inside the fridge.

This separation of concerns and each person (service) managing their own business area is IMO what microservices should be all about.

This is an oversimplification and doesn't answer your advanced questions, but it's just a way of thought of how microservices map to the real world that might help.

2
RaidionOP•

So this is a great analogy. But in this case, the same team (4 devs) owns the service (and a few workers) that touch the database. In our case, there is an API that provides external access, and workers that handles async messages that we care about. The team owns all code that touches that datastore, and any other team that wants to touch the data store has to do it through the API.

The lines are drawn around the domain, not one repo.

1
[deleted]•

[removed]

1
boot_spring•

RemindMe! 9 hours

0
RemindMeBot•

There is a 50.0 minute delay fetching comments.

I will be messaging you in 8 hours on 2020-03-05 05:13:31 UTC to remind you of this link

CLICK THIS LINK to send a PM to also be reminded and to reduce spam.

^(Parent commenter can ) ^(delete this message to hide from others.)


^(Info)^(Custom)^(Your Reminders)^(Feedback)
1