In modern backend development, we often follow a service-oriented architecture where each model is initialized in its own service. However, there are cases where a particular service needs access to a model defined in a different service. One common requirement in such scenarios is to perform a JOIN query between models residing in separate services.
In this guide, we'll explore how to add a JOIN query efficiently when your models are initialized separately and need to interact within a specific service.
When working with a microservices-based backend, models are often encapsulated within their respective services. For instance:
UserService manages the User
model.
OrderService manages the Order
model.
ProductService manages the Product
model.
If OrderService
needs to retrieve user details along with order data, it must perform a JOIN operation while keeping services decoupled.
If all models are using the same database, you can write raw SQL queries or use an ORM (like Sequelize, TypeORM, or Prisma) to perform the JOIN.
const { Sequelize, DataTypes } = require("sequelize");
const sequelize = new Sequelize("database", "user", "password", {
host: "localhost",
dialect: "postgres",
});
async function getOrdersWithUsers() {
const query = `
SELECT orders.id, orders.amount, users.name, users.email
FROM orders
INNER JOIN users ON orders.user_id = users.id;
`;
const results = await sequelize.query(query, { type: Sequelize.QueryTypes.SELECT });
return results;
}
getOrdersWithUsers().then(data => console.log(data));
If you're using an ORM like Sequelize, you can define relationships and fetch related data seamlessly.
const Order = sequelize.define("Order", {
id: { type: DataTypes.INTEGER, primaryKey: true },
user_id: { type: DataTypes.INTEGER },
amount: { type: DataTypes.FLOAT },
});
const User = sequelize.define("User", {
id: { type: DataTypes.INTEGER, primaryKey: true },
name: { type: DataTypes.STRING },
email: { type: DataTypes.STRING },
});
Order.belongsTo(User, { foreignKey: "user_id" });
async function getOrdersWithUsers() {
return await Order.findAll({
include: [{ model: User, attributes: ["name", "email"] }],
});
}
This approach keeps services independent and avoids tight coupling.
If models exist in separate services, the best approach is to make an API call from one service to another to fetch the required data.
UserService
in OrderService
const axios = require("axios");
async function getOrdersWithUsers() {
const orders = await Order.findAll();
const userIds = orders.map(order => order.user_id);
const usersResponse = await axios.post("http://userservice/api/users", { ids: userIds });
const usersData = usersResponse.data;
return orders.map(order => ({
...order.toJSON(),
user: usersData.find(user => user.id === order.user_id),
}));
}
This approach keeps services independent and avoids tight coupling.
Instead of making synchronous API calls, you can publish events when a record is created and store necessary data in a local copy within the service.
For example, when a new user is created in UserService
, an event can be published, and OrderService
can subscribe to store user details locally.
const redis = require("redis");
const subscriber = redis.createClient();
subscriber.subscribe("user_created");
subscriber.on("message", (channel, message) => {
if (channel === "user_created") {
const userData = JSON.parse(message);
UserCache.save(userData); // Store locally
}
});
This ensures efficient data retrieval without repeated API calls.
When integrating JOIN queries in a service-oriented architecture where models are initialized in separate services, different approaches can be applied:
Direct JOIN queries for a single database setup.
ORM-based relations for a structured monolithic approach.
API calls to fetch related data in microservices.
Event-driven architecture for real-time synchronization.
Choosing the right approach depends on your project’s architecture, scalability requirements, and performance considerations.
Which approach do you use for handling JOIN queries in a microservices setup? Let us know in the comments!