Storing Matches in DynamoDB with Commutative Hashing — wup

Storing Matches in DynamoDB with Commutative Hashing

How wup (our Tinder-style showcase app) stores a symmetric "match" between two users as a single deterministic record using Go + AWS SDK v2 and DynamoDB.

Problem statement

In a dating-style app like wup users can "like" each other. When both users like each other we want to record a match. Important constraints:

High-level solution

The simplest robust approach is to compute a commutative key from the two user IDs (UUIDs). A commutative function returns the same value regardless of argument order:

// compute(uuid1, uuid2) == compute(uuid2, uuid1)

We achieve that by:

  1. Sorting the two UUIDs lexicographically.
  2. Concatenating them with a separator.
  3. Hashing the result (SHA-256) to get a compact partition key.
Why sort first? Sorting guarantees determinism: both (A,B) and (B,A) become the same combined string before hashing.

Go: commutative hash function

// commutative_hash.go
package matchkey

import (
    "crypto/sha256"
    "encoding/hex"
    "sort"
)

// ComputeMatchHash returns a commutative hash for two UUIDs.
// computeMatchHash(a, b) == computeMatchHash(b, a)
func ComputeMatchHash(a, b string) string {
    ids := []string{a, b}
    sort.Strings(ids)
    combined := ids[0] + "-" + ids[1]
    sum := sha256.Sum256([]byte(combined))
    return hex.EncodeToString(sum[:])
}

Storing a match in DynamoDB (Go + AWS SDK v2)

// matches_dynamo.go
package main

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "log"
    "sort"
    "time"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func computeMatchHash(a, b string) string {
    ids := []string{a, b}
    sort.Strings(ids)
    combined := ids[0] + "-" + ids[1]
    sum := sha256.Sum256([]byte(combined))
    return hex.EncodeToString(sum[:])
}

func putMatch(ctx context.Context, client *dynamodb.Client, table string, userA, userB string) error {
    matchID := computeMatchHash(userA, userB)
    _, err := client.PutItem(ctx, &dynamodb.PutItemInput{
        TableName: &table,
        Item: map[string]types.AttributeValue{
            "matchId":   &types.AttributeValueMemberS{Value: matchID},
            "userA":     &types.AttributeValueMemberS{Value: userA},
            "userB":     &types.AttributeValueMemberS{Value: userB},
            "createdAt": &types.AttributeValueMemberS{Value: time.Now().UTC().Format(time.RFC3339)},
        },
        ConditionExpression: awsString("attribute_not_exists(matchId)"),
    })
    return err
}

func getMatch(ctx context.Context, client *dynamodb.Client, table, userA, userB string) (map[string]types.AttributeValue, error) {
    matchID := computeMatchHash(userA, userB)
    out, err := client.GetItem(ctx, &dynamodb.GetItemInput{
        TableName: &table,
        Key: map[string]types.AttributeValue{
            "matchId": &types.AttributeValueMemberS{Value: matchID},
        },
    })
    if err != nil {
        return nil, err
    }
    return out.Item, nil
}

func awsString(s string) *string { return &s }

func main() {
    ctx := context.Background()
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        log.Fatalf("loading AWS config: %v", err)
    }
    client := dynamodb.NewFromConfig(cfg)
    table := "Matches"
    userA := "550e8400-e29b-41d4-a716-446655440000"
    userB := "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
    if err := putMatch(ctx, client, table, userA, userB); err != nil {
        fmt.Println("match exists or error:", err)
    } else {
        fmt.Println("match created")
    }
    item, err := getMatch(ctx, client, table, userB, userA)
    if err != nil {
        log.Fatalf("get match: %v", err)
    }
    fmt.Printf("found match: %+v
", item)
}