├── .gitignore
├── Problem-Statement.md
├── README.md
├── attachments
└── cache-lld.png
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── codeyapa
│ ├── Main.java
│ ├── algorithms
│ ├── DoublyLinkedList.java
│ ├── DoublyLinkedListNode.java
│ └── exception
│ │ └── InvalidDataException.java
│ └── cache
│ ├── Cache.java
│ ├── exception
│ ├── InvalidStateException.java
│ ├── KeyNotFoundException.java
│ └── StorageFullException.java
│ ├── factory
│ └── CacheFactory.java
│ ├── policy
│ ├── EvictionPolicy.java
│ └── LRUEvictionPolicy.java
│ └── storage
│ ├── HashMapStorage.java
│ └── Storage.java
└── test
└── java
└── com
└── codeyapa
├── algorithms
└── DoublyLinkedListTest.java
└── cache
└── policy
└── LRUEvictionPolicyTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 | .idea
25 | target
26 | *.iml
27 |
--------------------------------------------------------------------------------
/Problem-Statement.md:
--------------------------------------------------------------------------------
1 | ## Problem Statement
2 | We have to do low level design for a Cache system. Cache that we will design will have to support following operations:
3 | * **Put**: This will allow user to put a value against a key in the cache.
4 | * **Get**: This will allow user to get the previously saved value using key.
5 | * **Eviction**: Cache should also support removal of some key in case cache is full, and we try to add new key value.
6 |
7 | ### Expectations
8 | * Code should be functionally correct.
9 | * Code should be modular and readable. Clean and professional level code.
10 | * Code should be extensible and scalable. Means it should be able to accommodate new requirements with minimal changes.
11 | * Code should have good OOPs design.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cache - Low level system design
2 | * Low level Design of a Cache
3 | * Extensible cache design
4 | * Eviction policies and key storages can be injected.
5 |
6 |
7 | ## Problem statement
8 | [Check here](Problem-Statement.md)
9 |
10 | ## Class Diagram
11 | 
12 |
13 |
--------------------------------------------------------------------------------
/attachments/cache-lld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/goyal27/cache-lld/27fdc74666ff813b3174580c3a84d89fc99ad5bb/attachments/cache-lld.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.example
8 | cache-lld
9 | 1.0-SNAPSHOT
10 |
11 |
12 |
13 | org.apache.maven.plugins
14 | maven-compiler-plugin
15 |
16 | 8
17 | 8
18 |
19 |
20 |
21 |
22 |
23 |
24 | org.projectlombok
25 | lombok
26 | 1.18.12
27 | provided
28 |
29 |
30 |
31 | com.google.collections
32 | google-collections
33 | 1.0
34 |
35 |
36 | org.junit.jupiter
37 | junit-jupiter
38 | RELEASE
39 | test
40 |
41 |
42 | org.junit.jupiter
43 | junit-jupiter-api
44 | 5.7.0-M1
45 | compile
46 |
47 |
48 |
49 | org.mockito
50 | mockito-all
51 | 1.10.19
52 | test
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/Main.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa;
2 |
3 | public class Main {
4 | public static void main(String[] args) {
5 |
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/algorithms/DoublyLinkedList.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.algorithms;
2 |
3 | import com.codeyapa.algorithms.exception.InvalidDataException;
4 |
5 | import java.util.Objects;
6 |
7 | public class DoublyLinkedList {
8 | private final DoublyLinkedListNode dummyHead;
9 | private final DoublyLinkedListNode dummyTail;
10 |
11 | public DoublyLinkedList() {
12 | this.dummyHead = new DoublyLinkedListNode<>(null);
13 | this.dummyTail = new DoublyLinkedListNode<>(null);
14 | this.dummyHead.next = dummyTail;
15 | this.dummyTail.prev = dummyHead;
16 | }
17 |
18 | public boolean isEmpty() {
19 | return dummyHead.next == dummyTail;
20 | }
21 |
22 | public void insertNodeAtEnd(final DoublyLinkedListNode node) {
23 | final DoublyLinkedListNode tailPrev = dummyTail.prev;
24 | dummyTail.prev = node;
25 | node.next = dummyTail;
26 | tailPrev.next = node;
27 | node.prev = tailPrev;
28 | }
29 |
30 | public DoublyLinkedListNode insertElementAtEnd(final E element) {
31 | if (Objects.isNull(element))
32 | throw new InvalidDataException();
33 | final DoublyLinkedListNode node = new DoublyLinkedListNode(element);
34 | insertNodeAtEnd(node);
35 | return node;
36 | }
37 |
38 | public void detachNode(final DoublyLinkedListNode node) {
39 | if (Objects.nonNull(node)) {
40 | node.prev.next = node.next;
41 | node.next.prev = node.prev;
42 | }
43 | }
44 |
45 | public DoublyLinkedListNode getFirstNode() {
46 | if (isEmpty())
47 | return null;
48 | return dummyHead.next;
49 | }
50 |
51 | public DoublyLinkedListNode getLastNode() {
52 | if (isEmpty())
53 | return null;
54 | return dummyTail.prev;
55 | }
56 |
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/algorithms/DoublyLinkedListNode.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.algorithms;
2 |
3 | import lombok.Getter;
4 | @Getter
5 | public class DoublyLinkedListNode {
6 | E element;
7 | DoublyLinkedListNode prev;
8 | DoublyLinkedListNode next;
9 |
10 | public DoublyLinkedListNode(E element) {
11 | this.element = element;
12 | this.prev = null;
13 | this.next = null;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/algorithms/exception/InvalidDataException.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.algorithms.exception;
2 |
3 | public class InvalidDataException extends RuntimeException{
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/Cache.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache;
2 |
3 | import com.codeyapa.cache.exception.InvalidStateException;
4 | import com.codeyapa.cache.exception.KeyNotFoundException;
5 | import com.codeyapa.cache.exception.StorageFullException;
6 | import com.codeyapa.cache.policy.EvictionPolicy;
7 | import com.codeyapa.cache.storage.Storage;
8 |
9 | import java.util.Objects;
10 |
11 | public class Cache {
12 | private final Storage storage;
13 | private final EvictionPolicy evictionPolicy;
14 |
15 | public Cache(Storage storage, EvictionPolicy evictionPolicy) {
16 | this.storage = storage;
17 | this.evictionPolicy = evictionPolicy;
18 | }
19 |
20 | public Value get(final Key key) {
21 | try {
22 | Value value = storage.get(key);
23 | evictionPolicy.keyAccessed(key);
24 | return value;
25 | } catch (KeyNotFoundException keyNotFoundException) {
26 | System.out.println("Hit a cahce miss for key " + key);
27 | }
28 | return null;
29 | }
30 |
31 | public void put(final Key key, final Value value) {
32 | try {
33 | storage.add(key, value);
34 | evictionPolicy.keyAccessed(key);
35 | } catch (StorageFullException storageFullException) {
36 | System.out.println("Got storage full! Trying to evict");
37 | Key keyToBeRemoved = evictionPolicy.evict();
38 | if (Objects.isNull(keyToBeRemoved)) {
39 | throw new InvalidStateException("Invalid State! No storage space left and no keys to evict");
40 | }
41 | storage.remove(keyToBeRemoved);
42 | System.out.println("Evicting key " + keyToBeRemoved);
43 | put(key, value);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/exception/InvalidStateException.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.exception;
2 |
3 | public class InvalidStateException extends RuntimeException{
4 | public InvalidStateException(String message){super(message);}
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/exception/KeyNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.exception;
2 |
3 | public class KeyNotFoundException extends RuntimeException{
4 | public KeyNotFoundException(String message){super(message);}
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/exception/StorageFullException.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.exception;
2 |
3 | public class StorageFullException extends RuntimeException{
4 | public StorageFullException(String message){super(message);}
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/factory/CacheFactory.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.factory;
2 |
3 | import com.codeyapa.cache.Cache;
4 | import com.codeyapa.cache.policy.LRUEvictionPolicy;
5 | import com.codeyapa.cache.storage.HashMapStorage;
6 |
7 | public class CacheFactory {
8 | public Cache defaultCache(final int capacity) {
9 | return new Cache<>(new HashMapStorage(capacity), new LRUEvictionPolicy());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/policy/EvictionPolicy.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.policy;
2 |
3 | public interface EvictionPolicy {
4 | void keyAccessed(Key key);
5 | Key evict();
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/policy/LRUEvictionPolicy.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.policy;
2 |
3 | import com.codeyapa.algorithms.DoublyLinkedList;
4 | import com.codeyapa.algorithms.DoublyLinkedListNode;
5 |
6 | import java.util.HashMap;
7 | import java.util.Map;
8 | import java.util.Objects;
9 |
10 | public class LRUEvictionPolicy implements EvictionPolicy {
11 | private final Map> map;
12 | private final DoublyLinkedList dll;
13 |
14 | public LRUEvictionPolicy() {
15 | map = new HashMap<>();
16 | dll = new DoublyLinkedList<>();
17 | }
18 |
19 | @Override
20 | public void keyAccessed(final Key key) {
21 | if (map.containsKey(key)) {
22 | dll.detachNode(map.get(key));
23 | dll.insertNodeAtEnd(map.get(key));
24 | } else {
25 | final DoublyLinkedListNode node = dll.insertElementAtEnd(key);
26 | map.put(key, node);
27 | }
28 | }
29 |
30 | @Override
31 | public Key evict() {
32 | final DoublyLinkedListNode lruNode = dll.getFirstNode();
33 | if (Objects.isNull(lruNode))
34 | return null;
35 | dll.detachNode(lruNode);
36 | map.remove(lruNode.getElement());
37 | return lruNode.getElement();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/storage/HashMapStorage.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.storage;
2 |
3 | import com.codeyapa.cache.exception.KeyNotFoundException;
4 | import com.codeyapa.cache.exception.StorageFullException;
5 |
6 | import java.util.HashMap;
7 |
8 | public class HashMapStorage implements Storage {
9 | private final HashMap storage;
10 | private final int capacity;
11 |
12 | public HashMapStorage(int capacity) {
13 | this.capacity = capacity;
14 | this.storage = new HashMap<>();
15 | }
16 |
17 | @Override
18 | public void add(final Key key, final Value value) throws StorageFullException {
19 | if (storageFull())
20 | throw new StorageFullException("Capacity Reached");
21 | storage.put(key, value);
22 | }
23 |
24 | @Override
25 | public void remove(Key key) {
26 | if (!storage.containsKey(key)) throw new KeyNotFoundException(key + "doesn't exist in cache.");
27 | storage.remove(key);
28 | }
29 |
30 | @Override
31 | public Value get(Key key) throws KeyNotFoundException {
32 | if (!storage.containsKey(key))
33 | throw new KeyNotFoundException("Key " + key+ " Not Found");
34 | return storage.get(key);
35 | }
36 |
37 | private boolean storageFull() {
38 | return storage.size() == capacity;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/codeyapa/cache/storage/Storage.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.storage;
2 |
3 | import com.codeyapa.cache.exception.KeyNotFoundException;
4 | import com.codeyapa.cache.exception.StorageFullException;
5 |
6 | public interface Storage {
7 | void add(Key key, Value value) throws StorageFullException;
8 | void remove(Key key) ;
9 | Value get(Key key) throws KeyNotFoundException;
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/java/com/codeyapa/algorithms/DoublyLinkedListTest.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.algorithms;
2 |
3 | import com.google.common.collect.ImmutableList;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.List;
7 |
8 | import static org.junit.jupiter.api.Assertions.*;
9 |
10 | class DoublyLinkedListTest {
11 | @Test
12 | void testDllNodesAddition() {
13 | DoublyLinkedListNode node1 = new DoublyLinkedListNode<>(1);
14 | DoublyLinkedListNode node2 = new DoublyLinkedListNode<>(1);
15 | DoublyLinkedListNode node3 = new DoublyLinkedListNode<>(1);
16 | DoublyLinkedListNode node4 = new DoublyLinkedListNode<>(1);
17 |
18 | DoublyLinkedList dll = new DoublyLinkedList<>();
19 |
20 | dll.insertNodeAtEnd(node1);
21 | verifyDLL(dll, ImmutableList.of(1));
22 |
23 | dll.insertNodeAtEnd(node2);
24 | verifyDLL(dll, ImmutableList.of(1, 2));
25 |
26 | dll.insertNodeAtEnd(node3);
27 | verifyDLL(dll, ImmutableList.of(1, 2, 3));
28 |
29 | dll.insertNodeAtEnd(node4);
30 | verifyDLL(dll, ImmutableList.of(1, 2, 3, 4));
31 | }
32 |
33 | void verifyDLL(DoublyLinkedList dll, List expectedListElements) {
34 | assertEquals(expectedListElements.get(expectedListElements.size() - 1), dll.getLastNode().getElement());
35 | assertEquals(expectedListElements.get(0), dll.getFirstNode().getElement());
36 |
37 | DoublyLinkedListNode currentNode = dll.getFirstNode();
38 | for (Integer expectedListElement : expectedListElements) {
39 | assertNotNull(currentNode);
40 | assertEquals(expectedListElement, currentNode.getElement());
41 | currentNode = currentNode.getNext();
42 | }
43 | assertNull(currentNode.next);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/com/codeyapa/cache/policy/LRUEvictionPolicyTest.java:
--------------------------------------------------------------------------------
1 | package com.codeyapa.cache.policy;
2 |
3 | import org.junit.jupiter.api.BeforeEach;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static org.junit.jupiter.api.Assertions.assertEquals;
7 | import static org.junit.jupiter.api.Assertions.assertNull;
8 |
9 | class LRUEvictionPolicyTest {
10 | private LRUEvictionPolicy lruEvictionPolicy;
11 |
12 | @BeforeEach
13 | void setUp() {
14 | lruEvictionPolicy = new LRUEvictionPolicy<>();
15 | }
16 |
17 | @Test
18 | void testNoKeyToEvictInitially() {
19 | assertNull(lruEvictionPolicy.evict());
20 | }
21 |
22 | @Test
23 | void testKeysAreEvictedInTheOrderInWhichTheyAreAccessedAccess() {
24 | lruEvictionPolicy.keyAccessed(1);
25 | lruEvictionPolicy.keyAccessed(2);
26 | lruEvictionPolicy.keyAccessed(3);
27 | lruEvictionPolicy.keyAccessed(4);
28 | assertEquals(1, lruEvictionPolicy.evict());
29 | assertEquals(2, lruEvictionPolicy.evict());
30 | assertEquals(3, lruEvictionPolicy.evict());
31 | assertEquals(4, lruEvictionPolicy.evict());
32 | }
33 |
34 | @Test
35 | void testReaccesingKeyPreventsItFromEviction() {
36 | lruEvictionPolicy.keyAccessed(1);
37 | lruEvictionPolicy.keyAccessed(2);
38 | lruEvictionPolicy.keyAccessed(3);
39 | lruEvictionPolicy.keyAccessed(2);
40 | lruEvictionPolicy.keyAccessed(4);
41 | lruEvictionPolicy.keyAccessed(1);
42 | lruEvictionPolicy.keyAccessed(5);
43 | assertEquals(3, lruEvictionPolicy.evict());
44 | assertEquals(2, lruEvictionPolicy.evict());
45 | assertEquals(4, lruEvictionPolicy.evict());
46 | assertEquals(1, lruEvictionPolicy.evict());
47 | assertEquals(5, lruEvictionPolicy.evict());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------