Data Structures
HashMap
Key-Value Pairs
Programming
Software Development

An interchangeable key/value HashMap Set structure

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

An "interchangeable key/value" map is usually a bidirectional map, often called a BiMap. The idea is simple: if A maps to 1, you want to be able to look up 1 and get back A with the same efficiency.

What Problem This Structure Solves

A normal HashMap gives you fast lookup in one direction only. If you also need fast reverse lookup, searching all entries is too slow.

The usual fix is to maintain two hash maps:

  • 'forward: key -> value'
  • 'reverse: value -> key'

That gives average O(1) lookup in both directions. The tradeoff is that every write must keep both maps consistent.

This only works cleanly when the mapping is one-to-one. If two different keys may share the same value, the reverse side is no longer a plain map.

Core Invariant

The data structure is correct only if these statements are always true:

  • Every key appears at most once in forward.
  • Every value appears at most once in reverse.
  • 'forward[k] == v if and only if reverse[v] == k.'

Once you think in terms of invariants, the implementation becomes straightforward. Insert, update, and remove operations all exist to preserve that relationship.

Java Implementation with Two HashMaps

The following example shows a small generic implementation. It rejects duplicate keys and duplicate values so the mapping remains invertible.

java
1import java.util.HashMap;
2import java.util.Map;
3
4public class BiMap<K, V> {
5    private final Map<K, V> forward = new HashMap<>();
6    private final Map<V, K> reverse = new HashMap<>();
7
8    public void put(K key, V value) {
9        if (forward.containsKey(key)) {
10            throw new IllegalArgumentException("Key already exists: " + key);
11        }
12        if (reverse.containsKey(value)) {
13            throw new IllegalArgumentException("Value already exists: " + value);
14        }
15        forward.put(key, value);
16        reverse.put(value, key);
17    }
18
19    public V getByKey(K key) {
20        return forward.get(key);
21    }
22
23    public K getByValue(V value) {
24        return reverse.get(value);
25    }
26
27    public V removeByKey(K key) {
28        V value = forward.remove(key);
29        if (value != null) {
30            reverse.remove(value);
31        }
32        return value;
33    }
34
35    public K removeByValue(V value) {
36        K key = reverse.remove(value);
37        if (key != null) {
38            forward.remove(key);
39        }
40        return key;
41    }
42
43    public int size() {
44        return forward.size();
45    }
46
47    public static void main(String[] args) {
48        BiMap<String, Integer> ids = new BiMap<>();
49        ids.put("alice", 101);
50        ids.put("bob", 102);
51
52        System.out.println(ids.getByKey("alice"));
53        System.out.println(ids.getByValue(102));
54
55        ids.removeByValue(101);
56        System.out.println(ids.getByKey("alice"));
57    }
58}

This is the standard design because it keeps both directions explicit and easy to reason about.

Updates Are Harder Than Reads

Reads are trivial. Updates are where bugs usually appear.

Suppose you want to replace alice -> 101 with alice -> 205. You must:

  1. Remove the old reverse entry for 101.
  2. Verify that 205 is not already assigned to someone else.
  3. Write the new pair to both maps.

If you update one side and forget the other, the structure becomes corrupt. That is why mature libraries often wrap bidirectional maps in a dedicated type instead of exposing two independent maps directly.

When You Need Sets Instead of Single Values

If values are not unique, the structure is not really interchangeable anymore. For example, if both alice and bob can belong to "admin", then reverse lookup must return a collection:

  • 'Map<K, V> for the forward direction'
  • 'Map<V, Set<K>> for the reverse direction'

That is still a valid design, but it is no longer a one-to-one BiMap. It is better described as dual indexes or two synchronized maps.

Existing Libraries

Before writing your own version, check whether your language already provides one:

  • Java: Guava has BiMap
  • Python: third-party packages such as bidict
  • C++: Boost has multi-index containers for similar needs

A library implementation usually handles corner cases such as replacement semantics, iteration, and view consistency more carefully than ad hoc code.

Common Pitfalls

The most common mistake is forgetting that bidirectional lookup requires value uniqueness. If multiple keys can share a value, reverse lookup cannot return a single answer.

Another mistake is updating only one map during removal or reassignment. That leaves stale entries behind and causes mismatched lookups later.

People also underestimate concurrency issues. Two maps must be updated atomically if the structure is shared across threads. If you need thread safety, protect both maps with the same lock or use a purpose-built concurrent abstraction.

Finally, do not describe this as a "set" unless the semantics are actually set-like. Most of the time the right term is bidirectional map.

Summary

  • An interchangeable key/value structure is usually a bidirectional map, not a special kind of HashSet.
  • The standard implementation uses two hash maps: one forward and one reverse.
  • The design only works cleanly when keys and values are both unique.
  • Writes must update both maps together to preserve the invariant.
  • If duplicates are allowed on either side, use maps of sets instead of a one-to-one BiMap.

Course illustration
Course illustration

All Rights Reserved.