---
# System prepended metadata

title: Bito Golang Interview Assignment 2.1

---

# Bito Golang Interview Assignment 2.1

# Answer 1
First of all this create deadlock issue.
Locking from and to sequentially `from.Lock.Lock() followed by to.Lock.Lock()` 
can lead to a deadlock when two goroutines simultaneously call transfer,
each with reversed user arguments.

one possible solution is to always lock the user with the smaller ID first.
this ensures that the locking order is consistent across all calls to preventing deadlocks.
for mitigating the deadlock issue we can use the below code.
```go
func transfer(from *User, to *User, amount uint64) {
	// consistent lock order
	var first, second *User
	if from.ID < to.ID {
		first, second = from, to
	} else {
		first, second = to, from
	}

	first.Lock.Lock()
	second.Lock.Lock()
	defer first.Lock.Unlock()
	defer second.Lock.Unlock()

	if from.Balance >= amount {
		from.Balance -= amount
		to.Balance += amount
	}
}
```

If we don't want to use ID then we can use try lock or global lock instead.
here is the code for tryLock
```go
func transfer(from *User, to *User, amount uint64) {
	for {
		from.Lock.Lock()
		if to.Lock.TryLock() {
			break
		}
		from.Lock.Unlock()
        
		// backoff and retry
		time.Sleep(10 * time.Millisecond) 
	}

	defer from.Lock.Unlock()
	defer to.Lock.Unlock()

	if from.Balance >= amount {
		from.Balance -= amount
		to.Balance += amount
	}
}
```

other minor issue
* significant notation `10e10` which is a float64 by default and not an integer.
* symbol too large for the array size to compile.


# Answer 2
For storing user purchases in Redis, the List data structure may be suitable.
List in Redis maintain the order of insertion and allow fast appends (like LPUSH or RPUSH) with trimming operations.
according to the redis docs,
LPUSH / RPUSH are O(1) operations and LTRIM is O(N) where N is the number of elements in the list.
so if we do LPUSH and LTRIM in the same operation, it will be O(1) operation when purchase arrives. 
also for minimizing memory usage, we can use LTRIM to keep only the last 100 purchases.
```
LPUSH user:{userID}:purchase {purchaseID}
LTRIM user:{userID}:purchase 0 99
```

when reading the recent 100 purchased products, we can use LRANGE to get the list of purchases.
according to the redis docs,
LRANGE is O(S+N) where S is the start offset and N is the number of elements in the range.
so if we keep the range fixed to 0-99, it will be O(1) operation (S=0, N=100).
```
LRANGE user:{userID}:purchase 0 99
```    

if recent 100 purchased products API should contain more purchase details,
we can store the purchase details in list with serialized data,
but keep in mind, we would like to minimize memory usage, so we should store only necessary details.


# Answer 3

In re-create strategy involves taking down the existing version of the application before deploying the new one. All pods are terminated before any new pods are created. This can be acceptable for development environments but is generally unsuitable for production unless downtime is not a concern.

In a rolling update, the new version of the application is gradually rolled out without taking down the entire system. This means that new pods are created with the new version, and old pods are terminated in a controlled manner.
This strategy aims to ensure that the service remains available during the update. It's designed to update one pod at a time, slowly replacing old pods with new ones while maintaining service availability and minimize downtime.

Readiness probes are used in deployments to manage rolling updates effectively. They help determine when a pod is ready to handle requests, which is metters during an update to ensure continuous availability and minimal service disruption. The new version of Pods is rolled out and older Pods are not terminated until the new version has Pods in ready state. These probes also inform services about which pods to include in their endpoints.


# Answer 4
The optimal index would closely align with the filters and conditions specified in the query 
to minimize the number of rows scanned and to allow the database engine to efficiently locate the needed data.
The reason is that indexes are used to minimize the number of disk reads, and in many database systems, 
they are structured as B-trees or similar data structures that are efficient for range queries.

Index B orders data by user_id, created_at, status, this matches the structure of the query. where user_id is likely an exact match condition, 
created_at is a range condition, and status is another exact match condition. 
Index B allows the database to first narrow down data by user_id, 
perform a range scan on created_at, and finally filter by status. 
This order optimizes the use of exact match followed by a range condition, which is generally efficient. It can take advantage of the index order if the conditions in the WHERE clause are listed in the same order as the index columns.


# Answer 5
Kafka scales consumer-side performance by leveraging consumer groups
A consumer group can have multiple consumers, each consuming messages from a subset of partitions within a topic. This distributes the processing load across multiple consumers, improving overall throughput.
we can easily add more consumers to a consumer group to handle increased message volume. This horizontal scaling allows you to match the processing power to the incoming data stream.

there are some drawbacks to consider:
When a consumer joins or leaves a group, Kafka rebalances partitions across remaining consumers. This can cause temporary slowdowns as consumers adjust to the new configuration. For limited parallelism per partition, only one consumer can consume messages from a single partition at a time. The maximum number of consumers in a group is limited by the number of partitions in the topic.
Kafka manages consumer group coordination ensuring that each partition is processed by exactly one consumer within the group. This eliminates the issue of duplicate processing or message loss.


To address the issue of a fixed number of consumers per topic, Kafka administrators can dynamically adjust the number of partitions based on the workload and the number of active consumers. This requires careful planning and monitoring.
Kafka allows developers to define custom partitioning strategies. This can be used to distribute messages more evenly across partitions based on specific attributes of the messages, mitigating the problem of uneven load.
With incremental cooperative rebalancing in new versions of Kafka introduced a more efficient rebalancing protocol that allows for incremental rebalancing of consumers. This reduces the rebalancing overhead and improves the stability during changes in the consumer group.


# Answer 6
GitHub repository link:
https://github.com/jynychen/tinder-match
