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:
- Store the match as a single record (no duplicate rows like
A-BandB-A). - Allow either user to fetch that match using the same deterministic key.
- Keep reads/writes simple and efficient in DynamoDB.
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:
- Sorting the two UUIDs lexicographically.
- Concatenating them with a separator.
- 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)
}