Using LUTs (Look-Up Tables) Data Structures to simplify code’s logic and more
Tags: programming, go, tutorial
Published: 2023-12-30 07:06:38

## Issues

When I was working on a feature to check the transaction’s status from a third-party service, I had to deal with plenty of possible combinations of values that could be returned by the service. The code was getting more and more complex, with each response status have its own description and action to be taken. Here is a simplified version of the code:

```go
func (s *thirdParty) CheckTransactionStatus(
    /* request */ ctx context.Context, request CheckTransactionStatusRequest) (
    /* response */ response CheckTransactionStatusResponse, httpStatusCode int, err error,
) {
    resp, err := s.client.CheckTransactionStatus(ctx, request)
    if err != nil {
        return response, http.StatusInternalServerError, err
    }

    switch resp.Status {
        case "SUCCESS":
            return response, http.StatusOK, nil
        case "PENDING":
            return response, http.StatusAccepted, nil
        case "FAILED":
            return response, http.StatusBadGateway, nil
        case "REJECTED":
            return response, http.StatusForbidden, nil
        case "CANCELLED":
            return response, http.StatusGone, nil
        case "EXPIRED":
            return response, http.StatusGone, nil
        case "REFUNDED":
            return response, http.StatusGone, nil
        case "CHARGEBACK":
            return response, http.StatusGone, nil
        case "ERROR":
            return response, http.StatusBadGateway, nil
        default:
            return response, http.StatusInternalServerError, errors.New("unknown status")   
    }
}
```

The code above was performing many conditional checks to return the correct HTTP status code. It also had a lot of duplicated code. That makes me ask myself;

- what if I need to add a new status code?
- what if I need to add a new description? Or action to be taken?
- can I add in-depth logging to the code? And update it easily in the future?

## Solution

I decided to use a LUT (Look-Up Table) data structure to simplify the code’s logic. The LUT is a data structure that associates keys with values. It is a simple way to replace a lot of if/else statements or switch/case statements.

### Phase 1: Using a LUT to replace if/else statements

```go
var transactionStatus = map[string]int{
    "SUCCESS": http.StatusOK,
    "PENDING": http.StatusAccepted,
    "FAILED": http.StatusBadGateway,
    "REJECTED": http.StatusForbidden,
    "CANCELLED": http.StatusGone,
    "EXPIRED": http.StatusGone,
    "REFUNDED": http.StatusGone,
    "CHARGEBACK": http.StatusGone,
    "ERROR": http.StatusBadGateway,
}

func (s *thirdParty) CheckTransactionStatus(
    /* request */ ctx context.Context, request CheckTransactionStatusRequest) (
    /* response */ response CheckTransactionStatusResponse, httpStatusCode int, err error,
) {
    resp, err := s.client.CheckTransactionStatus(ctx, request)
    if err != nil {
        return response, http.StatusInternalServerError, err
    }

    httpStatusCode, found := transactionStatus[resp.Status]
    if !found {
        return response, http.StatusInternalServerError, errors.New("unknown status")
    }

    return response, httpStatusCode, nil
}
```

The code above is a lot simpler than the previous one. It is also easier to add a new status code.

### Phase 2: Using a LUT to add in-depth logging

```go
var transactionStatus = map[string]struct {
    httpStatusCode int
    logLevel       string
    description    string
}{
    "SUCCESS": {
        httpStatusCode: http.StatusOK,
        logLevel:       "info",
        description:    "transaction was successful",
    },
    "PENDING": {
        httpStatusCode: http.StatusAccepted,
        logLevel:       "info",
        description:    "transaction is pending",
    },
    "FAILED": {
        httpStatusCode: http.StatusBadGateway,
        logLevel:       "error",
        description:    "transaction failed",
    },
    "REJECTED": {
        httpStatusCode: http.StatusForbidden,
        logLevel:       "error",
        description:    "transaction was rejected",
    },
    "CANCELLED": {
        httpStatusCode: http.StatusGone,
        logLevel:       "error",
        description:    "transaction was cancelled",
    },
    "EXPIRED": {
        httpStatusCode: http.StatusGone,
        logLevel:       "error",
        description:    "transaction was expired",
    },
    "REFUNDED": {
        httpStatusCode: http.StatusGone,
        logLevel:       "error",
        description:    "transaction was refunded",
    },
    "CHARGEBACK": {
        httpStatusCode: http.StatusGone,
        logLevel:       "error",
        description:    "transaction was charged back",
    },
    "ERROR": {
        httpStatusCode: http.StatusBadGateway,
        logLevel:       "error",
        description:    "transaction had an error",
    },
}

func (s *thirdParty) CheckTransactionStatus(
    /* request */ ctx context.Context, request CheckTransactionStatusRequest) (
    /* response */ response CheckTransactionStatusResponse, httpStatusCode int, err error,
) {
    resp, err := s.client.CheckTransactionStatus(ctx, request)
    if err != nil {
        return response, http.StatusInternalServerError, err
    }

    status, found := transactionStatus[resp.Status]
    if !found {
        return response, http.StatusInternalServerError, errors.New("unknown status")
    }

    log.WithFields(log.Fields{
        "status":      resp.Status,
        "description": status.description,
    }).Info("transaction status")

    return response, status.httpStatusCode, nil
}
```

## Pros and Cons

Like any other data structure, LUTs have their pros and cons.

### Pros

1. **Fast Retrieval:**
   - Look-up tables provide constant-time average complexity for retrieving values based on a key. This makes them very efficient for large datasets.

2. **Memory Efficiency:**
   - Look-up tables can be memory-efficient, especially when dealing with sparse data. They only store values for existing keys, reducing memory overhead.

3. **Simplifies Code:**
   - Using a look-up table can make code more readable and maintainable. It centralizes related information and makes it easy to update or extend.

4. **Pre-computation:**
   - Look-up tables allow for pre-computation and storage of results, reducing the need for recalculations. This is beneficial in scenarios where certain values are repeatedly used.

5. **Flexible Key-Value Mapping:**
   - The key-value nature of a look-up table allows for flexible mappings, supporting a wide range of use cases from configuration settings to status code mappings.

### Cons

1. **Memory Overhead:**
   - In scenarios where the key space is large or continuous, a look-up table might consume more memory than alternative data structures with more memory-efficient representations.

2. **Limited Sorting:**
   - Look-up tables are inherently unordered, and sorting based on keys may require additional data structures or steps.

3. **Limited Range Queries:**
   - If you need to perform range queries on keys, look-up tables might not be the most suitable data structure, and alternatives like trees might be more appropriate.

## Conclusion

Look-up tables are a simple and powerful data structure that can be used to simplify code logic. They are fast, memory-efficient, and flexible. They can be used in a wide range of use cases, from configuration settings to status code mappings. However, they are not suitable for all scenarios, and you should consider the pros and cons before using them.