Problem Statement: How would you go about writing an application in Java where employee id to department name lookup is cached instead of reading from a database or an API. The cache needs to be refreshed every 1 minute.
Solution, step by step
Step 1: Firstly, a simplified config class that reads config values from a “.properties” or a “.yml” file.
AppConfig.java:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package com.myapp; /** * Singleton config class */ public class AppConfig { private static AppConfig instance; private String hostName; private Integer port; private String apiPath; private Integer cacheReloadTimeInMinutes; //cannot be instantiated from outside private AppConfig(){} public static synchronized AppConfig getInstance(){ if(instance == null){ instance = new AppConfig(); instance.loadConfig(); } return instance; } private void loadConfig() { //some logic to read from a config file. instance.hostName = "ahost.com"; instance.port = 8080; instance.apiPath = "/v1/anapi/employee"; cacheReloadTimeInMinutes = 1; } public String getHostName() { return hostName; } public Integer getPort() { return port; } public String geApiPath() { return this.apiPath; } public Integer getCacheReloadTimeInMinutes() { return cacheReloadTimeInMinutes; } } |
Step 2: The Data Access Object to read the data from an API or a database.
Firstly, the interface EmployeeDao.java:
|
1 2 3 4 5 6 7 8 |
package com.myapp; import java.util.Map; public interface EmployeeDao { Map<String, String> loadEmployee(); } |
Next, the implementation of the interface EmployeeDaoImpl.java:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.myapp; import java.util.HashMap; import java.util.Map; /** * Data Access Object (Dao) to read data from a say REST API */ public class EmployeeDaoImpl implements EmployeeDao { private AppConfig config; public EmployeeDaoImpl(AppConfig config) { this.config = config; } public Map<String, String> loadEmployee() { // invoke an API by taking URL details from config. String url = "https://" + config.getHostName() + ":" + config.getPort() + config.geApiPath(); //...........logic to load api data. // let's hard code for now Map<String, String> map = new HashMap<>(10); map.put("emp123", "sales"); map.put("emp345", "marketing"); map.put("emp678", "finance"); map.put("emp999", "sales"); return map; } } |
Step 3: Now the caching class that initially loads the data when it is constructed in the main thread, and then refreshes the data every X minutes in a separate scheduled thread.
The interface EmployeeCacheManager.java
|
1 2 3 4 5 6 7 8 9 10 11 |
package com.myapp; public interface EmployeeCacheManager { String getDepartment(String empId); boolean exists(String empId); void start(); void shutdown(); } |
The implementation class EmployeeCacheManagerImpl.java:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
package com.myapp; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class EmployeeCacheManagerImpl implements EmployeeCacheManager { private final AtomicBoolean started = new AtomicBoolean(false); private ScheduledExecutorService scheduledThreadPool; private Map<String, String> empDeptCache; private final AppConfig config; private final EmployeeDao employeeDao; public EmployeeCacheManagerImpl(AppConfig config, EmployeeDao employeeDao) { this.config = config; this.employeeDao = employeeDao; loadWithRetries(5, 2); } @Override public String getDepartment(String empId){ return empDeptCache.get(empId); } @Override public boolean exists(String empId){ return empDeptCache.containsKey(empId); } private void loadWithRetries(int retries, int delay) { for (int i = 0; i < retries; i++) { load(); if(empDeptCache != null && !empDeptCache.isEmpty()){ return; } try { TimeUnit.SECONDS.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } new RuntimeException("Error loading to cache"); } private void load() { System.out.println("Started loading the cache ....in thread "+ Thread.currentThread().getName()); Map<String, String> result = employeeDao.loadEmployee(); if(!result.isEmpty()){ synchronized (this) { empDeptCache = result; System.out.println("Finished loading the cache ...."); } } } @Override public void start() { if(started.compareAndSet(false, true)) { ThreadFactory tf = new ThreadFactoryBuilder().setNameFormat("EmployeeCacheLoader-t-%d") .setDaemon(true) .build(); scheduledThreadPool = Executors.newScheduledThreadPool(1, tf); scheduledThreadPool.scheduleWithFixedDelay(this::load, config.getCacheReloadTimeInMinutes(), config.getCacheReloadTimeInMinutes(), TimeUnit.MINUTES); } } @Override public void shutdown(){ if(started.compareAndSet(true, false)) { scheduledThreadPool.shutdown(); try { scheduledThreadPool.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (!scheduledThreadPool.isTerminated()) { scheduledThreadPool.shutdownNow(); } } } } } |
Step 4: Finally, the main application that can can be executed on the man thread.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.myapp; import java.util.concurrent.TimeUnit; public class AppMain { public static void main(String[] args) throws InterruptedException { AppConfig config = AppConfig.getInstance(); EmployeeDao dao = new EmployeeDaoImpl(config); EmployeeCacheManager cacheMgr = new EmployeeCacheManagerImpl(config, dao); cacheMgr.start(); //do something with the application System.out.println("Lookup for data in cache...." + cacheMgr.getDepartment("emp678")); TimeUnit.MINUTES.sleep(5); cacheMgr.shutdown(); } } |
Outputs:
Let it run for 5 minutes & then exit.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Started loading the cache ....in thread main Finished loading the cache .... Lookup for data in cache....finance Started loading the cache ....in thread EmployeeCacheLoader-t-0 Finished loading the cache .... Started loading the cache ....in thread EmployeeCacheLoader-t-0 Finished loading the cache .... Started loading the cache ....in thread EmployeeCacheLoader-t-0 Finished loading the cache .... Started loading the cache ....in thread EmployeeCacheLoader-t-0 Finished loading the cache .... Process finished with exit code 0 |