How to Add JOIN Queries in Services with Separate Model Initialization

Learn how to implement JOIN queries efficiently when working with separate service-based models in microservices architecture. Explore SQL, ORM, API calls, and event-driven approaches.

How to Add JOIN Queries in Services with Separate Model Initialization
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.
Understanding the Problem
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.
Approaches to Implementing JOIN Queries
1. Using Direct Database Query in the Service
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.
Example with SQL Query (Using PostgreSQL & Sequelize)
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));
2. Using ORM with Associations (Best for Monolithic Services)
If you're using an ORM like Sequelize, you can define relationships and fetch related data seamlessly.
Sequelize Example with Associations
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.
3. Using API Calls Between Services (Best for Microservices Architecture)
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.
Example: Fetching User Data from 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.
4. Using an Event-Driven Approach (Best for High Scalability)
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.
Example: Using Redis Pub/Sub for Event-Based Sync
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.
Conclusion
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!