DEV Community

Concurrency Control in Go with Ent ORM and MySQL

In this article, we'll delve into the world of concurrency control in Go, specifically focusing on the optimistic locking approach. We'll explore its implementation using Ent ORM to illustrate how to manage data consistency when multiple users interact with the same resource. Keep in mind that this example serves as a simplified illustration, and real-world booking systems involve a many of additional complexities. However, the core concepts presented here provide a solid foundation for understanding optimistic locking in Go applications. Feel free to explore the complete source code in my GitHub repository for a more in-depth look at the implementation.

white biplane

Corbin was incredibly excited for his long vacation. He'd been looking forward to exploring Europe, especially the city of Rome in Italy, and witnessing the Colosseum alongside other historical landmarks. He even booked his plane tickets through JustLetGo.com, splurging on a business-class seat for a comfortable and unforgettable flying experience.

However, fate had other plans. On his departure day, Corbin hit a snag at the security checkpoint. The staff informed him that his assigned seat was already occupied by another passenger named Miles. Corbin's disappointment was undeniable. His meticulously planned vacation suddenly turned chaotic.

Upon investigation, it turned out there was a data error in JustLetGo.com's system. Both Corbin and Miles had booked the same seat and departure time, resulting in boarding passes being printed for both passengers. JustLetGo.com sincerely apologized and offered Corbin a free flight to Italy with a first-class seat as compensation.

While Corbin did end up with a higher class, his disappointment as a customer due to JustLetGo.com's unreliable booking system remained. This incident could negatively impact JustLetGo.com in several ways, such as:

  • Bad customer experience
  • Negative reviews from customers
  • Financial loss
  • Potentially losing customers forever

Corbin's story serves as a valuable lesson for JustLetGo.com to improve their booking system and ensure their customers' comfort. Corbin's disappointment could turn into loyalty if JustLetGo.com handles such situations effectively and provides satisfying solutions.

The Double Booking Problem

The incident you just read about is known as The Double Booking Problem. It occurs when two people reserve the same accommodation service at the same time. This problem is not limited to flight booking systems; it can also occur in hotel booking systems, concert tickets, personal trainer appointments, doctor appointments, and so on. The main causes of this problem are not limited to unreliable booking systems but it's also possible because manual errors or miscommunication between different parties involved in the accommodation.

Let's assume we are the engineering team of the JustLetGo.com website and we use Go, Ent ORM, and MySQL for our booking system. Let's start discussing how double booking occurs by looking at the schema and tables before any passenger has booked a flight:

// ent/schema/seat.go
func (Seat) Fields() []ent.Field {
    return []ent.Field{
        field.Int("id"),
        field.Bool("is_booked").Default(false),
        field.String("passenger_name").Optional().Nillable(),
    }
}
Enter fullscreen mode Exit fullscreen mode
mysql> select * from seats;
+----+-----------+----------------+
| id | is_booked | passenger_name |
+----+-----------+----------------+
|  1 |         0 | NULL           |
|  2 |         0 | NULL           |
|  3 |         0 | NULL           |
|  4 |         0 | NULL           |
|  5 |         0 | NULL           |
|  6 |         0 | NULL           |
|  7 |         0 | NULL           |
|  8 |         0 | NULL           |
|  9 |         0 | NULL           |
| 10 |         0 | NULL           |
| 11 |         0 | NULL           |
| 12 |         0 | NULL           |
| 13 |         0 | NULL           |
| 14 |         0 | NULL           |
| 15 |         0 | NULL           |
| 16 |         0 | NULL           |
| 17 |         0 | NULL           |
| 18 |         0 | NULL           |
| 19 |         0 | NULL           |
| 20 |         0 | NULL           |
+----+-----------+----------------+
20 rows in set (0.00 sec)
Enter fullscreen mode Exit fullscreen mode

The field is_booked indicates whether the airplane seat has been reserved by the passenger_name. The example above shows when no seats on the plane have been reserved yet.

When a visitor to the JustLetGo.com website purchases a plane ticket, the system will book a seat by sending a request as follows:

POST /seats HTTP/1.1
Host: api.justletego.com
Content-Type: application/json

{
  "seatID": 2,
  "passengerName": "Corbin"
}
Enter fullscreen mode Exit fullscreen mode

The system will then process the request with the following flow:

// domain/seats/usecaases/book_seat.go
func (us *UseCases) BookSeat(ctx context.Context, in InBookSeat) (*OutBookSeat, error) {
    // Start Ent's transaction
    tx, err := us.db.Tx(ctx)
    if err != nil {
        return nil, err
    }

    // Find and check seat availability
    requestedSeat, err := tx.Seat.Query().Where(seat.ID(in.SeatID)).Only(ctx)
    if err != nil {
        tx.Rollback()
        return nil, err
    }

    if requestedSeat.IsBooked {
        tx.Rollback()
        return nil, ErrSeatIsBooked
    }

    // Reserve a seat
    _, err = tx.Seat.
        Update().
        Where(seat.ID(in.SeatID)).
        SetIsBooked(true).
        SetPassengerName(in.PassengerName).
        Save(ctx)

    if err != nil {
        tx.Rollback()
        return nil, err
    }

    // Return the updated data from the transaction.
    updatedSeat, err := tx.Seat.
        Query().
        Where(seat.ID(in.SeatID)).
        Only(ctx)
    if err != nil {
        tx.Rollback()
        return nil, err
    }
    tx.Commit()

    out := OutBookSeat{}
    err = mapstructure.Decode(updatedSeat, &out)
    return &out, err
}
Enter fullscreen mode Exit fullscreen mode

At first glance, the flow of the BookSeat function above looks good, right? Inside the transaction, there is a query to get information about the airplane seat requested by the client, then there is a check whether the seat has been booked or not, and if it has been booked, an error will occur with the status that the seat has been booked.

Have you found the bug yet? If not, let's discuss it further by looking this code and query result.

requestedSeat, err := tx.Seat.Query().Where(seat.ID(in.SeatID)).Only(ctx)
Enter fullscreen mode Exit fullscreen mode
mysql> select * from seats where id = 2;
+----+-----------+----------------+
| id | is_booked | passenger_name |
+----+-----------+----------------+
|  2 |         0 | NULL           |
+----+-----------+----------------+
1 row in set (0.05 sec)
Enter fullscreen mode