0

I am working on a small project with microservices architecture in Spring Boot. As to not make unnecessary calls to users-microservice, I have duplicated some necessary User data (id, name, summary, photo) also into the posts-microservice, by having an Entity named UserExt ('external').

The posts-microservice contains these Entities:

@Table(name = "posts")
public class Post {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// the table that holds the id of the User that made the Post. Used the wording 'ext' for External, to differentiate the
// data that is not in this microservice (or database, but for simplicity I use 1 DB, even though each microservice,
// uses its own set of tables). However, since it would be inefficient to call users microservice to retrieve user data when
// building a post response, we also store some crucial user data in this ms as well.
@ManyToOne
@JoinColumn(name = "user_ext", referencedColumnName = "id", nullable = false)
private UserExt userExt;

// --- other post related fields
}

@Table(name = "user_ext")
public class UserExt {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, name = "user_id_ext")
private Long userIdExt;

@Column(nullable = false, name = "user_email_ext")
private String userEmail;

@Column(name = "first_name", nullable = false)
private String firstName;

@Column(name = "last_name", nullable = false)
private String lastName;

@Column(name = "short_summary")
private String shortSummary;

@Column(name = "profile_photo")
private String profilePhoto;

}

The issue I am facing is this:

If I chose to use POST /posts to create a new post, the client will not need to give any ID and I can get the username/email of the currently authenticated user to identify the user that the created post belongs to. However, this way, unless I make a call to users-microservice, I will not be able to get the userId. One way would be to only store the email/username of the user in the UserExt entity. However, this would be an issue with the below scenario.

Another scenario: If GET /users/userId/posts is used to get all the posts of a certain user and I only store the username/email of user in the UserExt, how would I be able to satisfy this case.

What would be the best way to handle these cases?

2
  • 2
    If you have a situation where the primary responsibilities of two microservices are inter-related and inter-dependent upon each other, then it's quite strongly suggests those services may have fallen into a "distributed monolith" anti-pattern, and that those services should ideally be combined together into a single service instead -- newrelic.com/blog/best-practices/… Commented May 27, 2022 at 11:52
  • You also have the option of using something like GraphQL which can pull data from across multiple services and present it as one unified piece. If handled properly, the GraphQL layer can cache so you aren't issuing queries for the same user over and over. Commented May 27, 2022 at 15:50

2 Answers 2

1

I'm assuming you are using JWTs for authentication. You can store userId, username and email in the JWT. However, you will need to have user data from user service.

The one to one relationship is probably wrong though. One user can have many posts right? In this case you can try to save the post with that userId, if that fails because of foreign key constraint violation (user does not exist in the posts db), then you can fetch the user from user service and save it to the database and save the post again.

Now you are dealing with redundant data, which means that you will have to have a way of telling the posts service that user data was updated on user service. This is done with some message broker - kafka, rabbitmq, etc. - where you emit "UserUpdated" event from user service and listen for this event in posts service and update user information accordingly.

I think that this is how microservices are done properly, however, this means a lot of work is required and a lot of complexity is introduced.

If you want to reduce complexity, don't have redundant data, but just store userId. So Post entity will have userId, which you will then resolve into the user by calling the other service. This simplifies your design, although it is a bit more inefficient and adds a bit of complexity if you want to query your posts.

The main takeaway is to stay away from microservices if possible.

4
  • Hi, thanks for your answer and explanation. You are right, I should store userId into JWT and probably store the data into a Bean when making a request into the microservice. Also, the relationship should be ManyToOne,not OneToOne. Regarding the workflow, I was thinking about populating the User data in the posts-microservice, through an Event when an user is created by users-ms. This way I would always have the most important user data into the posts-microservice as well. However, I am aware that this means having a significant amount of redundant data.
    – don
    Commented May 27, 2022 at 11:52
  • I think the last line of this answer is in no way helpful. There are cases where microservices are warranted and needed. If the OP can very well be in that situation. Commented May 27, 2022 at 15:48
  • @don In that case you will already have a table of users ready, so you just save the post with user id and you will have the relationship correctly saved. If you can "afford" having this much data duplicated, then yeah, go for emitting events when user is created. The only difference between my flow and yours is that in my flow user is created when he is "needed" in the posts ms instead of eagerly :)
    – Blaž Mrak
    Commented May 28, 2022 at 1:42
  • @BerinLoritsch that is why the "if possible". Microservices are the best way to nuke your project (most of ms are done badly, even at big companies). If you have a small team (<8-10), if you don't have an experienced enough team, etc. it really pays off to think about it. He might be in such situation, but a "monolith" will take you very far (further than you expect). If you care for MS, start with loosely coupled monolith, as you will have more wiggleroom. Then you can break the monolith up into smaller pieces when needed. It is a better strategy than big upfront ms design.
    – Blaž Mrak
    Commented May 28, 2022 at 2:01
0

One of the core principles about microservices is that they are independently deploy-able. There are additional disciplines that are required to make that a reality, but let's restate the requirements that are necessary in this scenario:

  1. Posts must be associated with a user
  2. Assumption: it's a UI requirement to see poster email, etc.
  3. Assumption: we want to see the current user information
  4. We must minimize the number of synchronous calls

There's some inferred requirements we want to include here based on the fact we are using microservices.

  1. We do not want to leak implementation details for user information (i.e. replicate that data in different services)
  2. We want one authoritative source of user information (i.e. the user service)

With all of that in mind, there are already a couple technologies that help solve those problems. The first issue is associating posts with a user.

  • JWT can contain more than the user ID. It should have everything necessary to validate the user has the correct permissions (i.e. attributes and roles)
  • The JWT should only be valid for a short period to avoid easy spoofing
  • The Posts service would be storing a reference to the User. I.e. the sub claim in the JWT.
  • The Post service's POST handler would require the JWT to exist in the Authorization HTTP header (or application specific header), and that the JWT is validated and the user is authorized to perform the action before continuing

Also, providing a layer to view your data as a graph can avoid extra calls when necessary. Currently, the most prominent solution for that is GraphQL. Historically there was also Falcore, but it's maintainers abandoned it in favor of GraphQL.

  • When properly configured, GraphQL solves the N+1 problem (i.e. 1 query to get a list of results, and 1 query for each row to look up associated information)
  • The Schema allows the client to request the size and shape of data (i.e. limit the fields to what is needed in the front end)
  • The GraphQL layer allows you to view the information in your Posts service and your User service as if they were one related group.
  • You can easily introduce caching in this layer to speed up common queries

Obviously there are more than one way of solving this problem. This combination avoids the complexity of asynchronous updates to notify services when data changes.

4
  • Won't this DOS the user service?
    – Blaž Mrak
    Commented May 28, 2022 at 1:54
  • No. Also, if needed you can scale just the user service horizontally. Like I said, GraphQL solves the N+1 problem. Between implementing caching and only requesting the unique user records displayed on screen, it's a pretty elegant solution for the problem. Commented May 28, 2022 at 11:17
  • It is not the service that worries me, it is the DB. GraphQL isn't a magic stick, it increases the complexity of the backend significantly (at least as far as I understand it), adds it's own cache, so you have duplicated data anyways except now you don't really know when it is out of date or am I missing something? Also, please excuse my stupidity here, but is GraphQL on another service or does each service implement GraphQL API?
    – Blaž Mrak
    Commented May 28, 2022 at 14:49
  • It's a tool that we've implemented in a product my team has created. In our case, it was it's own service. In Netflix's case, each service has it's own GraphQL interface, and the global GraphQL service uses something called schema stitching to break a big query into the smaller ones. We didn't have the need to go that far. So it can work both ways. For our system it did really help out with better query planning to minimize the things the front end needed to ask from the back end. Commented May 28, 2022 at 16:28

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.