Skip to content

Commit eb8f9db

Browse files
iluwatarohbus
andauthored
iluwatar#590 add explanation for caching pattern (iluwatar#1693)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
1 parent 6d7084f commit eb8f9db

File tree

3 files changed

+319
-9
lines changed

3 files changed

+319
-9
lines changed

caching/README.md

Lines changed: 310 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,326 @@ tags:
1010
---
1111

1212
## Intent
13-
To avoid expensive re-acquisition of resources by not releasing
14-
the resources immediately after their use. The resources retain their identity, are kept in some
15-
fast-access storage, and are re-used to avoid having to acquire them again.
13+
14+
The caching pattern avoids expensive re-acquisition of resources by not releasing them immediately
15+
after use. The resources retain their identity, are kept in some fast-access storage, and are
16+
re-used to avoid having to acquire them again.
17+
18+
## Explanation
19+
20+
Real world example
21+
22+
> A team is working on a website that provides new homes for abandoned cats. People can post their
23+
> cats on the website after registering, but all the new posts require approval from one of the
24+
> site moderators. The user accounts of the site moderators contain a specific flag and the data
25+
> is stored in a MongoDB database. Checking for the moderator flag each time a post is viewed
26+
> becomes expensive and it's a good idea to utilize caching here.
27+
28+
In plain words
29+
30+
> Caching pattern keeps frequently needed data in fast-access storage to improve performance.
31+
32+
Wikipedia says:
33+
34+
> In computing, a cache is a hardware or software component that stores data so that future
35+
> requests for that data can be served faster; the data stored in a cache might be the result of
36+
> an earlier computation or a copy of data stored elsewhere. A cache hit occurs when the requested
37+
> data can be found in a cache, while a cache miss occurs when it cannot. Cache hits are served by
38+
> reading data from the cache, which is faster than recomputing a result or reading from a slower
39+
> data store; thus, the more requests that can be served from the cache, the faster the system
40+
> performs.
41+
42+
**Programmatic Example**
43+
44+
Let's first look at the data layer of our application. The interesting classes are `UserAccount`
45+
which is a simple Java object containing the user account details, and `DbManager` which handles
46+
reading and writing of these objects to/from MongoDB database.
47+
48+
```java
49+
@Setter
50+
@Getter
51+
@AllArgsConstructor
52+
@ToString
53+
public class UserAccount {
54+
private String userId;
55+
private String userName;
56+
private String additionalInfo;
57+
}
58+
59+
@Slf4j
60+
public final class DbManager {
61+
62+
private static MongoClient mongoClient;
63+
private static MongoDatabase db;
64+
65+
private DbManager() { /*...*/ }
66+
67+
public static void createVirtualDb() { /*...*/ }
68+
69+
public static void connect() throws ParseException { /*...*/ }
70+
71+
public static UserAccount readFromDb(String userId) { /*...*/ }
72+
73+
public static void writeToDb(UserAccount userAccount) { /*...*/ }
74+
75+
public static void updateDb(UserAccount userAccount) { /*...*/ }
76+
77+
public static void upsertDb(UserAccount userAccount) { /*...*/ }
78+
}
79+
```
80+
81+
In the example, we are demonstrating various different caching policies
82+
83+
* Write-through writes data to the cache and DB in a single transaction
84+
* Write-around writes data immediately into the DB instead of the cache
85+
* Write-behind writes data into the cache initially whilst the data is only written into the DB
86+
when the cache is full
87+
* Cache-aside pushes the responsibility of keeping the data synchronized in both data sources to
88+
the application itself
89+
* Read-through strategy is also included in the aforementioned strategies and it returns data from
90+
the cache to the caller if it exists, otherwise queries from DB and stores it into the cache for
91+
future use.
92+
93+
The cache implementation in `LruCache` is a hash table accompanied by a doubly
94+
linked-list. The linked-list helps in capturing and maintaining the LRU data in the cache. When
95+
data is queried (from the cache), added (to the cache), or updated, the data is moved to the front
96+
of the list to depict itself as the most-recently-used data. The LRU data is always at the end of
97+
the list.
98+
99+
```java
100+
@Slf4j
101+
public class LruCache {
102+
103+
static class Node {
104+
String userId;
105+
UserAccount userAccount;
106+
Node previous;
107+
Node next;
108+
109+
public Node(String userId, UserAccount userAccount) {
110+
this.userId = userId;
111+
this.userAccount = userAccount;
112+
}
113+
}
114+
115+
/* ... omitted details ... */
116+
117+
public LruCache(int capacity) {
118+
this.capacity = capacity;
119+
}
120+
121+
public UserAccount get(String userId) {
122+
if (cache.containsKey(userId)) {
123+
var node = cache.get(userId);
124+
remove(node);
125+
setHead(node);
126+
return node.userAccount;
127+
}
128+
return null;
129+
}
130+
131+
public void set(String userId, UserAccount userAccount) {
132+
if (cache.containsKey(userId)) {
133+
var old = cache.get(userId);
134+
old.userAccount = userAccount;
135+
remove(old);
136+
setHead(old);
137+
} else {
138+
var newNode = new Node(userId, userAccount);
139+
if (cache.size() >= capacity) {
140+
LOGGER.info("# Cache is FULL! Removing {} from cache...", end.userId);
141+
cache.remove(end.userId); // remove LRU data from cache.
142+
remove(end);
143+
setHead(newNode);
144+
} else {
145+
setHead(newNode);
146+
}
147+
cache.put(userId, newNode);
148+
}
149+
}
150+
151+
public boolean contains(String userId) {
152+
return cache.containsKey(userId);
153+
}
154+
155+
public void remove(Node node) { /* ... */ }
156+
public void setHead(Node node) { /* ... */ }
157+
public void invalidate(String userId) { /* ... */ }
158+
public boolean isFull() { /* ... */ }
159+
public UserAccount getLruData() { /* ... */ }
160+
public void clear() { /* ... */ }
161+
public List<UserAccount> getCacheDataInListForm() { /* ... */ }
162+
public void setCapacity(int newCapacity) { /* ... */ }
163+
}
164+
```
165+
166+
The next layer we are going to look at is `CacheStore` which implements the different caching
167+
strategies.
168+
169+
```java
170+
@Slf4j
171+
public class CacheStore {
172+
173+
private static LruCache cache;
174+
175+
/* ... details omitted ... */
176+
177+
public static UserAccount readThrough(String userId) {
178+
if (cache.contains(userId)) {
179+
LOGGER.info("# Cache Hit!");
180+
return cache.get(userId);
181+
}
182+
LOGGER.info("# Cache Miss!");
183+
UserAccount userAccount = DbManager.readFromDb(userId);
184+
cache.set(userId, userAccount);
185+
return userAccount;
186+
}
187+
188+
public static void writeThrough(UserAccount userAccount) {
189+
if (cache.contains(userAccount.getUserId())) {
190+
DbManager.updateDb(userAccount);
191+
} else {
192+
DbManager.writeToDb(userAccount);
193+
}
194+
cache.set(userAccount.getUserId(), userAccount);
195+
}
196+
197+
public static void clearCache() {
198+
if (cache != null) {
199+
cache.clear();
200+
}
201+
}
202+
203+
public static void flushCache() {
204+
LOGGER.info("# flushCache...");
205+
Optional.ofNullable(cache)
206+
.map(LruCache::getCacheDataInListForm)
207+
.orElse(List.of())
208+
.forEach(DbManager::updateDb);
209+
}
210+
211+
/* ... omitted the implementation of other caching strategies ... */
212+
213+
}
214+
```
215+
216+
`AppManager` helps to bridge the gap in communication between the main class and the application's
217+
back-end. DB connection is initialized through this class. The chosen caching strategy/policy is
218+
also initialized here. Before the cache can be used, the size of the cache has to be set. Depending
219+
on the chosen caching policy, `AppManager` will call the appropriate function in the `CacheStore`
220+
class.
221+
222+
```java
223+
@Slf4j
224+
public final class AppManager {
225+
226+
private static CachingPolicy cachingPolicy;
227+
228+
private AppManager() {
229+
}
230+
231+
public static void initDb(boolean useMongoDb) { /* ... */ }
232+
233+
public static void initCachingPolicy(CachingPolicy policy) { /* ... */ }
234+
235+
public static void initCacheCapacity(int capacity) { /* ... */ }
236+
237+
public static UserAccount find(String userId) {
238+
if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) {
239+
return CacheStore.readThrough(userId);
240+
} else if (cachingPolicy == CachingPolicy.BEHIND) {
241+
return CacheStore.readThroughWithWriteBackPolicy(userId);
242+
} else if (cachingPolicy == CachingPolicy.ASIDE) {
243+
return findAside(userId);
244+
}
245+
return null;
246+
}
247+
248+
public static void save(UserAccount userAccount) {
249+
if (cachingPolicy == CachingPolicy.THROUGH) {
250+
CacheStore.writeThrough(userAccount);
251+
} else if (cachingPolicy == CachingPolicy.AROUND) {
252+
CacheStore.writeAround(userAccount);
253+
} else if (cachingPolicy == CachingPolicy.BEHIND) {
254+
CacheStore.writeBehind(userAccount);
255+
} else if (cachingPolicy == CachingPolicy.ASIDE) {
256+
saveAside(userAccount);
257+
}
258+
}
259+
260+
public static String printCacheContent() {
261+
return CacheStore.print();
262+
}
263+
264+
/* ... details omitted ... */
265+
}
266+
```
267+
268+
Here is what we do in the main class of the application.
269+
270+
```java
271+
@Slf4j
272+
public class App {
273+
274+
public static void main(String[] args) {
275+
AppManager.initDb(false);
276+
AppManager.initCacheCapacity(3);
277+
var app = new App();
278+
app.useReadAndWriteThroughStrategy();
279+
app.useReadThroughAndWriteAroundStrategy();
280+
app.useReadThroughAndWriteBehindStrategy();
281+
app.useCacheAsideStategy();
282+
}
283+
284+
public void useReadAndWriteThroughStrategy() {
285+
LOGGER.info("# CachingPolicy.THROUGH");
286+
AppManager.initCachingPolicy(CachingPolicy.THROUGH);
287+
var userAccount1 = new UserAccount("001", "John", "He is a boy.");
288+
AppManager.save(userAccount1);
289+
LOGGER.info(AppManager.printCacheContent());
290+
AppManager.find("001");
291+
AppManager.find("001");
292+
}
293+
294+
public void useReadThroughAndWriteAroundStrategy() { /* ... */ }
295+
296+
public void useReadThroughAndWriteBehindStrategy() { /* ... */ }
297+
298+
public void useCacheAsideStategy() { /* ... */ }
299+
}
300+
```
301+
302+
Finally, here is some of the console output from the program.
303+
304+
```
305+
12:32:53.845 [main] INFO com.iluwatar.caching.App - # CachingPolicy.THROUGH
306+
12:32:53.900 [main] INFO com.iluwatar.caching.App -
307+
--CACHE CONTENT--
308+
UserAccount(userId=001, userName=John, additionalInfo=He is a boy.)
309+
----
310+
```
16311

17312
## Class diagram
313+
18314
![alt text](./etc/caching.png "Caching")
19315

20316
## Applicability
317+
21318
Use the Caching pattern(s) when
22319

23-
* Repetitious acquisition, initialization, and release of the same resource causes unnecessary performance overhead.
320+
* Repetitious acquisition, initialization, and release of the same resource cause unnecessary
321+
performance overhead.
322+
323+
## Related patterns
324+
325+
* [Proxy](https://java-design-patterns.com/patterns/proxy/)
24326

25327
## Credits
26328

27329
* [Write-through, write-around, write-back: Cache explained](http://www.computerweekly.com/feature/Write-through-write-around-write-back-Cache-explained)
28330
* [Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching](https://docs.oracle.com/cd/E15357_01/coh.360/e15723/cache_rtwtwbra.htm#COHDG5177)
29331
* [Cache-Aside pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
332+
* [Java EE 8 High Performance: Master techniques such as memory optimization, caching, concurrency, and multithreading to achieve maximum performance from your enterprise applications](https://www.amazon.com/gp/product/178847306X/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=178847306X&linkId=e948720055599f248cdac47da9125ff4)
333+
* [Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond](https://www.amazon.com/gp/product/1492056111/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1492056111&linkId=7e553581559b9ec04221259e52004b08)
334+
* [Effective Java](https://www.amazon.com/gp/product/B078H61SCH/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=B078H61SCH&linkId=f06607a0b48c76541ef19c5b8b9e7882)
335+
* [Java Performance: The Definitive Guide: Getting the Most Out of Your Code](https://www.amazon.com/gp/product/1449358454/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1449358454&linkId=475c18363e350630cc0b39ab681b2687)

caching/src/main/java/com/iluwatar/caching/AppManager.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.text.ParseException;
2727
import java.util.Optional;
28+
import lombok.extern.slf4j.Slf4j;
2829

2930
/**
3031
* AppManager helps to bridge the gap in communication between the main class and the application's
@@ -33,6 +34,7 @@
3334
* Depending on the chosen caching policy, AppManager will call the appropriate function in the
3435
* CacheStore class.
3536
*/
37+
@Slf4j
3638
public final class AppManager {
3739

3840
private static CachingPolicy cachingPolicy;
@@ -50,7 +52,7 @@ public static void initDb(boolean useMongoDb) {
5052
try {
5153
DbManager.connect();
5254
} catch (ParseException e) {
53-
e.printStackTrace();
55+
LOGGER.error("Error connecting to MongoDB", e);
5456
}
5557
} else {
5658
DbManager.createVirtualDb();

0 commit comments

Comments
 (0)