The application lets users to create polls and share it with others. Once users cast their vote they can view the results in real-time i.e the poll result graph in Frontend would get updated real-time for all users as votes are being casted.
Here's a short video that explains the project and how it uses Redis:
The application consists of three repositories
The API server is responsible for all CRUD operations on Poll entity.
The socket service uses Socket.IO . All users are connect to a socket room. The room name is the poll id they are answering for.
As you guessed it is the frontend application build with React as a SPA. It uses the Socket io client for websockets and Plotly for charts.
When a Request to create a new Poll is made we create two data structures in Redis. A RedisJSON and a Hash.
- RedisJSON (Namespace
Poll:<pollId>
).
The essential poll data like pollId (entityId), title and poll options are stored as RedisJSON. This cloud be extended to include other meta data related to poll. The app uses Redis OM to store the poll data
import { Entity, Schema } from 'redis-om';
class Poll extends Entity {}
const pollSchema = new Schema(
Poll,
{
name: { type: 'string' },
options: { type: 'string[]' },
isClosed: { type: 'boolean' },
},
{ dataStructure: 'JSON' }
);
Below is the create function which stores the data using Redis OM
import { Client } from 'redis-om';
const create = async (name: string, options: string[]): Promise<string> => {
const redisOm = await new Client().use(redis);
const pollRepo = redisOm.fetchRepository(pollSchema);
const poll = pollRepo.createEntity()
poll.name = name;
poll.options = options;
poll.isClosed = false;
const id = await pollRepo.save(poll);
return id;
};
Here's how the created data looks on RedisInsight
- Hash (Namespace
pollBox:<pollId>
).
This stores the actual vote count for each poll Once the Poll Entity is created we use the same entityId to create the pollBox using the below function to create the hash.
const create = async (poll: Poll) => {
const entityId = poll.entityId;
const pollBoxId = `pollBox:${entityId}`;
const promises: Array<Promise<number>> = [];
poll.options.forEach((option) => {
promises.push(redis.hSet(pollBoxId, option, 0));
});
await Promise.all(promises);
};
The actual redis command we are concerned here is redis.hSet(pollBoxId, option, 0)
. We are storing all the options with initial value as zero under the pollBox id.
The Frontend makes a GET request with pollId to get the poll data. Since we created the poll data using the Redis OM we can use the same to retrieve the data.
const get = async (entityId: string): Promise<Poll> => {
const redisOm = await new Client().use(redis);
const pollRepo = redisOm.fetchRepository(pollSchema);
const poll = await pollRepo.fetch(entityId);
return poll;
};
This is the place where the real-time part of the application comes into play. For this event we do couple of operations on both the backend services.
- API Service
- Update the hash (Increment vote count by one)
- LPUSH updated pollId to the queue
- Publish an update message to pub/sub
- Socket service
- pub/sub receives the update message
- RPOP the pollId from Queue
- Read latest data from poll hash
- Broadcast to socket room.
1. Update the hash
We increment the poll hash by 1 for the option sent in request payload. Since we have used Redis Hash we can increment with a single command. Below is the function to increment and get the updated hash.
const update = async (entityId: string, option: string, count: number) => {
const pollBoxId = `pollBox:${entityId}`;
await redis.hIncrBy(pollBoxId, option, count);
const pollBox = await redis.hGetAll(pollBoxId);
return pollBox;
};
redis.hIncrBy(pollBoxId, option, count)
command increments the option by the value provided in count, here we use 1 for each vote.redis.hGetAll(pollBoxId)
Gets the updated hash data.
2. Update the Queue
const QUEUE_NAME = 'queue:polls';
const addPollIdToQueue = async (pollId: string) => {
await redis.lPush(QUEUE_NAME, pollId);
};
The command redis.lPush(QUEUE_NAME, pollId)
simply pushes the pollId to a Redis list named queue:polls
later in the socket service we will rPop
the items from this list thus treating this list like a Queue structure. Redis List data-types Docs
3. Publish Update to channel
const CHANNEL_NAME = 'channel:poll';
const UPDATE = 'update';
redis.publish(CHANNEL_NAME, UPDATE);
The command redis.publish(CHANNEL_NAME, UPDATE)
simply publishes an update message to the redis pub/sub later this will be consumed in the Socket service.
Socket Service
Socket service is subscribed to the Redis channel channel:poll
. Once an update message is received from the channel we RPOP the pollId from the queue queue:polls
and fetch the latest vote count form the hash and broadcasts the same to pollId room.
1. Subscribe to Redis pub/sub channel
(async () => {
const { CHANNEL_NAME } = constants;
const { POLL_UPDATE } = constants.SOCKET_EVENTS;
const subscribeClient = redis.duplicate();
await subscribeClient.connect();
await subscribeClient.subscribe(CHANNEL_NAME, async (message: string) => {
const pollId = await popPollQueue();
const pollBox = await getPollBox(pollId);
io.to(pollId).emit(POLL_UPDATE, { entityId: pollId, pollBox });
});
})();
The code subscribeClient.subscribe(channelName, callback)
is the piece which subscribes to the channel and when a new message is received the callback is executed.
2. Read Queue
As mentioned in the API Service we will RPOP the list to get the updated pollID.
// popPollQueue.ts
...
const pollId = await redis.rPop(QUEUE_NAME);
...
3. Read latest data from poll hash
// getPollBox.ts
...
const pollBoxId = `pollBox:${entityId}`;
const pollBox = await redis.hGetAll(pollBoxId);
...
redis.hGetAll()
gets the complete hash object.
- Clone all the three repos
- Create a
.env
file in the API service root directory - Create a
.env
file in the Socket service root directory - Both env file should have the Redis connection string with the name
REDIS_CONNECTION_STRING
// Example .env file
REDIS_CONNECTION_STRING = redis://username:password@redis-11983.c274.us-east-1-3.ec2.cloud.redislabs.com:11983
- Run
npm install
for all the three repos - Run the command
npm run dev
in all the repos to start the servers locally
Node.js min version 16
Here some resources to help you quickly get started using Redis Stack. If you still have questions, feel free to ask them in the Redis Discord or on Twitter.
- Sign up for a free Redis Cloud account using this link and use the Redis Stack database in the cloud.
- Based on the language/framework you want to use, you will find the following client libraries:
- Redis OM .NET (C#)
- Watch this getting started video
- Follow this getting started guide
- Redis OM Node (JS)
- Watch this getting started video
- Follow this getting started guide
- Redis OM Python
- Watch this getting started video
- Follow this getting started guide
- Redis OM Spring (Java)
- Watch this getting started video
- Follow this getting started guide
- Redis OM .NET (C#)
The above videos and guides should be enough to get you started in your desired language/framework. From there you can expand and develop your app. Use the resources below to help guide you further:
- Developer Hub - The main developer page for Redis, where you can find information on building using Redis with sample projects, guides, and tutorials.
- Redis Stack getting started page - Lists all the Redis Stack features. From there you can find relevant docs and tutorials for all the capabilities of Redis Stack.
- Redis Rediscover - Provides use-cases for Redis as well as real-world examples and educational material
- RedisInsight - Desktop GUI tool - Use this to connect to Redis to visually see the data. It also has a CLI inside it that lets you send Redis CLI commands. It also has a profiler so you can see commands that are run on your Redis instance in real-time
- Youtube Videos
- Official Redis Youtube channel
- Redis Stack videos - Help you get started modeling data, using Redis OM, and exploring Redis Stack
- Redis Stack Real-Time Stock App from Ahmad Bazzi
- Build a Fullstack Next.js app with Fireship.io
- Microservices with Redis Course by Scalable Scripts on freeCodeCamp