├── .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 | ![Class Diagram](attachments/cache-lld.png) 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 | --------------------------------------------------------------------------------