I got this question during an interview. The question itself was very open-ended - it asked me to implement a ticket queue system, where by default it would have 3 different queues to hold tickets that are of severity 1, severity 2, and severity 3 respectively. It should have functionalities like:
- add a ticket to the corresponding queue
- get the ticket from the highest severity (priority) queue
- resolve ticket i.e. remove the ticket from the queue
- loop over the tickets starting at the highest severity to the lowest severity
- check if a ticket is in the queue.
Finally, the design should be easily extensible to adapt future changes e.g. adding a new queue.
It doesn't have any predefined data structure for either the queue or the ticket so we have to come up with our own design. Here is how I implemented:
First for the tickets I have a class Ticket
:
class Ticket {
constructor({ name, desc = '', severity }) {
this.id = Math.floor(Math.random() * 1000)
this.timestamp = Date.now()
this.name = name
this.desc = desc
this.severity = severity
}
}
And for the ticket queues I have this:
class TicketQueues {
constructor(numOfQueues = 3) {
this.queues = Array.from({ length: numOfQueues }, () => [])
this.hashSet = new Set()
}
addTicket(ticket) {
if(this.hasTicket(ticket)) return false
const severityIndex = ticket.severity - 1
this.queues[severityIndex].push(ticket)
this.hashSet.add(ticket)
return true
}
*getTicketBySeverity() {
for (const queue of this.queues) {
for (const ticket of queue) {
yield ticket
}
}
}
getTicketAtHighestSeverity() {
for (const queue of this.queues) {
for (const ticket of queue) {
return ticket
}
}
}
resolveTicket(ticket) {
if(!this.hasTicket(ticket)) return false
const [severity, index] = this._findTicketIndex(ticket)
this.queues[severity].splice(index, 1)
this.hashSet.delete(ticket)
return true
}
_findTicketIndex(ticket) {
for (let i = 0; i < this.queues.length; i++) {
for (let j = 0; j < this.queues[i].length; j++) {
if (this.queues[i][j] === ticket) {
return [i, j]
}
}
}
return [-1, -1]
}
hasTicket(ticket) {
return this.hashSet.has(ticket)
}
}
The idea is that I have a two-dimensional array to represent the ticket queue system. The first array inside the two-dimensional array has the highest severity.
i.e. [ [ticket1] , [ticket2], [ticket3]]
means we have one ticket for severity 1 and 2 and 3
And I also have a hash set to hold a reference to every ticket so I can achieve constant time look up. For getTicketBySeverity
I implemented a generator function and the idea here is to take advantage of the lazy evaluation of generators since the data set might be huge so the user might not want to iterate through the whole list of tickets.
Please feel free to give me any feedback that you think might be helpful. There are a few design decision that I couldn't really think through all of the pros and cons and I would like you to give me some suggestion on the following design choices:
- As I mentioned I used two-dimensional array to hold the tickets, and it worked out fine. However one alternative I can think of is to use a hash map. so instead of having
[ [ticket1] , [ticket2], [ticket3]]
we have
{
sev1: [ticket1],
sev2: [ticket2],
sev3: [ticket3],
...
}
I couldn't really pinpoint exactly what are some of pros and cons of using either one data structure. One argument I can think of that is against using a hash map is that there might be an issue when looping through all of the tickets in the order of severity since normally hash map doesn't have the notion of order for keys. Another way I can think of is to have separate variable for each queue. so
class TicketQueues {
constructor(numOfQueues = 3) {
this.sev1Queue = []
this.sev2Queue = []
this.sev3Queue = []
}
However it seems cumbersome to me but I still am not super clear what exactly is bad about this design.
I am using reference equality to find the ticket instead of using an
id
. Not sure in real world scenario which approach is better. I guess usingid
is better since it is not always possible to keep the original reference for the object?To bucket the ticket to the right queue, the current implementation relies on the user to specify the correct severity i.e.
1
,2
,3
. If they use some weird to represent the severity then it would break e.g.A
B
C
. I guess this can be partially addressed by using TypesScript with eitherEnum
orUnion type
so the user will know the severity's type. However another approach I thought of is to expose specific API for each valid severity queue we have for the user to use. For example,
class TicketQueues {
constructor(numOfQueues = 3) {
this.queues = Array.from({length: numOfQueues}, () => [])
}
addSev1(ticket) {
this.queues[0].push(ticket)
}
addSev2(ticket) {
this.queues[1].push(ticket)
}
addSev3(ticket) {
this.queues[2].push(ticket)
}
but again I cannot really pinpoin exactly which approach is better. The second approach feels like it is hard for us to extend the queues if we ever need to add another queue, since we would have to add another method for the new queue.
the current implementation is inherently susceptible to starvation. e.g. as long as the sev1 queue is not empty, we will always start with tickets in sev1 queue when looping over the system even when there might a sev2 ticket that has been there for a long long time.
Lastly, I wonder if we can use priority queues instead of plain old queues here?