Backend Interview Question that I've got (Java, Spring)

By Kaden Sungbin Cho
Picture of the author
Published on
backend interview questions

Even the bitterness of failure can be viewed as a good experience when things go well.

I tried to find more accurate answers to the questions I received, and organized them so that the time I spent on the interview could be of some help to myself.


A

Oracle join types?

It was simply a question about Inner, Outer, Left, Right [1], etc.

I couldn't answer well because I was thinking too much about Broadcast Join, Nested loop, etc.

Connector other than Tomcat ajp Connector?

8 Standard HTTP Connectors (NIO, NIO2, APR) [3] and AJP Connector exist.

Structure of Tomcat?

A structure composed of objects such as Service, Engine, Host, Context, and Connector. Implementation of Servlet container specification.

Image from [2]
Image from [2]

Creating a queue with two stacks? How to improve performance?

One for insertion and one for reversing [4]. The only performance improvement that came to mind was lazy.

Timeout type in Connection [5]

connect: 3-way handshake must finish within timeout period

read: The client must receive a response from the server within the timeout period.

write: The client must successfully write to the server within the timeout period.

HTTP keep-alive (client side): As part of the HTTP header, if the client does not receive a response from the server within the timeout period, a request is sent to check whether the server is alive. If dead, disconnect the connection.

Request (server side HTTP): If the server does not receive data from the client for more than timeout time, the server drops the connection (I am not sure if the terminology is correct, it seems to be looking at HTTP keep-alive from the server's perspective)

If the Web Server's timeout is very long, there are countless requests, the thread pool is infinite, and the task is just an infinite sleep, which resource will be exhausted first?

Probably CPU-bound [6]

What happens when TCP Keepalive is long?

The new connection you are trying to enter cannot be created due to unavailability of resources.

How does Spring MVC work?

It is a servlet implementation that exchanges requests and responses between Servlet - DispatcherServlet - Controller.

Image from [7]
Image from [7]

Why use MQ?

I was trying to answer an article by Jay Kreps, the creator of Kafka, regarding the role of logs (an image came to mind).

Because it is MQ, the advantages of Kafka have many limitations (multiple consumption, reducing complexity, etc.), so the answer is that there is a lot of room to eliminate dependencies between forward and backward. (It seems to be roughly similar to the point of decouple [9])

Before - Image from [8]
Before - Image from [8]
After - Image from [8]
After - Image from [8]

How to restore the state of data in event sourcing form?

It is said that things like write-ahead log can be streamed in the form of event sourcing and replicated through replay.

Tools such as CDC are also mentioned [10].

B

Interface vs abstract class [12]

I only mentioned parts that I lightly understood, but additional questions came in using the default method as an example. It seemed like they were expecting some kind of answer related to Polymorphism.

When to use abstract class?

  • Used in relation to what an object is [13]
  • When trying to share state and functionality [14]
  • When using the concept of inheritance (although it is useless as the default method of the interface [12])
  • When trying to declare a non-public member (must be public in the interface)
  • When adding new methods later. If you add a new method to an interface, all classes that implement that interface must implement the new method.
  • When trying to create multiple versions of a component. Updating only the abstract class changes all classes that inherit it. When using an interface, it is difficult to change once created, so a new interface must be created for a new version.
  • Provides greater forward compatibility advantages. Once clients start using an interface, it is very difficult to change it.
  • When you want to provide common functions used by multiple components.

When to use Interface?

  • Used in relation to what an object can do (a type of contract) [13]
  • To provide promises for state and functionality [14]
  • Use interfaces for functions that are common to several unrelated objects.
  • Good choice if you don't think the API will change
  • When you want requirements similar to multiple inheritance
  • Used when designing a small, concise collection of features

Please explain what happens when you turn on the browser and access a specific site based on the OSI 7 layer perspective

[31, 32]

What is the difference between mutex and semaphore? [27, 28, 29, 30]

mutex

A mutex is a locking mechanism that provides mutual exclusion. The critical region is 'owned (Principle of Ownership)' by the task.

A mutex solves the following problems with a semaphore due to the existence of ownership:

  • Accidental release
  • recursive deadlock
  • deadlock through Task Death: Through Death detection -priority inversion
  • semaphore as a signal: mutex cannot be used for synchronization due to ownership. Therefore, clarify the purpose of use.

Mutexes inside some OSs may or may not support recursion, priority inheritance, and death detection depending on the implementation.

semaphore

A semaphore is a signaling mechanism that does not protect resources from access. Semaphore's Giving and Taking are fundamentally decoupled. There are two main types: binary and counting semaphore.

For example, in the case of a system that manages parking lots, the number of parking lots is initially set to count. Afterwards, the count decreases as the position is used. When the count reaches 0, parking for the next car is blocked. Afterwards, as one car leaves, the count increases to 1 and the waiting car can be used.

An example that illustrates the more signaling aspect is below:

   Task A                      Task B
   ...                         Take BinSemaphore   <== wait for something
   Do Something Noteworthy
   Give BinSemaphore           do something    <== unblocks

Task B waits for a specific event to occur. Task A takes some action to notify the occurrence of a specific event, and Task B confirms that.

In particular, in the binary semaphore, A releases the semaphore after B takes it. In this way, binary semaphore does not protect access to resources.

What is AOP? What are the main patterns of AOP?

The concept was explained well, but perhaps because I haven't used it, I couldn't answer the questions about what the pattern is and what's inside.

AOP is a programming paradigm that can isolate specific ‘aspects’ and increase modularity by cross-cutting [15].

The main pattern of AOP is Proxy, and Spring AOP uses jdk dynamic proxies as a standard. Additionally, CGLIB proxies can be used and annotations can be used using AspectJ [16].

If you look at proxy patterns, there are static and dynamic proxy patterns. [17]

Static is,

@Override directly or

package com.test.proxy;

public class Person {
    public void run(){
        System.out.println("Person Class run method");
    }
}
package com.test.proxy;

public class PersonStaticProxy extends Person {
    @Override
    public void run() {
        System.out.println("Person Class run Before method execution");
        super.run();
        System.out.println("Person Class run After the execution of the method");
    }
}
package com.test.proxy;

public class Test {
    public static void main(String[] args) {
        Person person = new PersonStaticProxy();
        person.run();
    }
}
Person Class run Before method execution
Person Class run method
Person Class run After the execution of the method

You can implement the interface and implementation target class by embedding it in the Proxy class as follows:

package com.test.proxy;

public interface MathematicDao {
    public void add(int a, int b);
}
package com.test.proxy;

public class MathematicImpl implements MathematicDao {
    @Override
    public void add(int a, int b) {
        System.out.println("a plus b The values of are:" + (a + b));
    }
}
package com.test.proxy;

public class MathematicProxy implements MathematicDao {
    MathematicImpl mathematic;

    public MathematicProxy( MathematicImpl mathematic){
        this.mathematic = mathematic;
    }

    @Override
    public void add(int a, int b) {
        System.out.println("add Before method execution");
        this.mathematic.add(a, b);
        System.out.println("add After the execution of the method");
    }
}
package com.test.proxy;

public class Test {
    public static void main(String[] args) {
        MathematicDao md = new MathematicProxy(new MathematicImpl());
        md.add(11, 22);
    }
}
add Before method execution
a plus b The value of is: 33
add After the execution of the method

However, if there are 100 target classes, 100 proxy classes must also be written, resulting in a lot of duplication.

Dynamic can be implemented using the jdk dynamic agent or cglib dynamic agent, but no modification is made to the target class method, whether dynamic or static.

First, the jdk dynamic agent base is:

package com.test.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DemoProxyHandler implements InvocationHandler{
    private Object obj;
    public DemoProxyHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println(method.getName() + "Before implementation");
        Object res = method.invoke(obj, args);
        System.out.println(method.getName() + "After implementation");

        return res;
    }
}
package com.test.proxy;

public interface MathematicDao {
    public void add(int a, int b);
}
package com.test.proxy;

public class MathematicImpl implements MathematicDao {
    @Override
    public void add(int a, int b) {
        System.out.println("a plus b The values of are:" + (a + b));
    }
}
package com.test.proxy;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        MathematicDao m = new MathematicImpl();

        DemoProxyHandler dp = new DemoProxyHandler(m);

        Object obj = Proxy.newProxyInstance(m.getClass().getClassLoader(), m.getClass().getInterfaces(), dp);

        MathematicDao mathematicDao = (MathematicDao) obj;

        mathematicDao.add(1, 3);
    }
}
add Before implementation
a plus b The value of is: 4
add After implementation

cglib uses low-level bytecode technology. Create a subclass for a class through bytecode technology, and in the subclass intercept all calls to parent class methods through method interception technology and perform cross-cutting logic:

package com.test.proxy;

public class CglibProxyHandler implements MethodInterceptor{

    private Object target;//The target object of the delegate

    public Object getInstance(Object target) {
        this.target = target;

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(target.getClass());

        enhancer.setCallback(this);

        return enhancer.create();
    }

    @Override
    public Object intercept(Object enhancer, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

        Object res = null;

        System.out.println(method.getName() + "Before method execution");

        try {
            res = methodProxy.invokeSuper(enhancer, args);//Equivalent to calling the original method
        }catch(Exception e) {
            e.printStackTrace();
            System.out.println(method.getName() + "Method execution exception");
        }

        System.out.println(method.getName() + "After the execution of the method");

        return res;
    }
}
package com.test.proxy;

public class Person {
    public void run(){
        System.out.println("Person Class run method");
    }
}
package com.test.proxy;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        CglibProxyHandler cph = new CglibProxyHandler();
      
        Person p = (Person) cph.getInstance(new Person());

        p.run();
    }
}
Person Class run Before method execution
Person Class run method
Person Class run After the execution of the method

The JDK can only proxy methods within a class that implements an interface. cglib uses inheritance-based proxies and cannot proxy static and final classes. cglib cannot proxy private or static methods.

AOP is a programming thinking method that enables non-intrusive extension of specific methods.

Spring AOP includes concepts such as Aspect, Join Point, Advice, Pointcut, Introduction, Target object, AOP Proxy, and weaving, and can be implemented roughly as follows:

Create the Advice class:

package com.test.proxy;

public class MethodAdvice {
    public void before(JoinPoint jp) {
        System.out.println("Before advice ");
    }

    public void after(JoinPoint jp) {
        System.out.println("Post notification");
    }

    public void afterReturning(JoinPoint jp, Object obj) {
        System.out.println("Notification after return, return value:" + obj);
    }

    public void afterThrowing(JoinPoint jp, Exception e) {
        System.out.println("Exception notification, abnormal:" + e.getMessage());
    }
}

config settings:

  <bean id="methodAdvice" class="com.test.proxy.MethodAdvice"></bean>
  
 <aop:config>
 
  <aop:pointcut expression="execution( * com.test.proxy.*.*(..))" id="pt"/>
   
   <aop:aspect ref="methodAdvice">
  
    <aop:before method="before" pointcut-ref="pt"/>
   
    <aop:after method="after" pointcut-ref="pt"/>
    
    <aop:after-throwing method="afterThrowing" pointcut-ref="pt" throwing="e"/>
    
    <aop:after-returning method="afterReturning" pointcut-ref="pt" returning="obj"/>
   </aop:aspect>
  </aop:config>
  
  <bean id="student" class="com.test.proxy.Student"></bean>
package com.test.proxy;

public class Student {
    public void study() {
        System.out.println("Student Class study method");
    }

    public String getStr(String s) {
        System.out.println("Student Class with return value");
        return s + "!!";
    }
}
package com.test.proxy;

public class Test {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

        Student stu= (Student) ctx.getBean("student");

        stu.study();

        stu.getStr("hello");
    }
}
Before advice 
Student Class study method
 Post notification
 Before advice 
Student Class with return value
 Post notification
 Notification after return, return value:hello!!

Python GIL

While talking about thread pools, we moved on to Python and a question about GIL came up. Strange answer.

The Global Interpreter Lock mutex allows only one thread to control the Python interpreter at any time. [18, 19]

Python uses reference counting for garbage collection, and if multiple threads change the reference count simultaneously, a reference leak occurs. To prevent that, you need to lock all shared objects and track references. Rather than locking the interpreter so that only one thread can change the reference count, it is easier and faster on a single thread.

If it is CPU-bound, it is very problematic, but most programs are IO-bound, and previously, lock time was distributed evenly for each thread, but in Python 3.2, it depends on the number of GIL acquisition requests of the thread (CPU-bound and IO-bound tasks can exist simultaneously). (to further distribute to CPU-bound).

There are other implementations besides CPython, so they can be used as alternatives (PyPy also has a GIL [20]).

Filter & HandlerInterceptor

Filter: Part of the webserver and not part of the Spring framework. You can use filters to manipulate requests or prevent them from reaching the servlet. Suitable for coarse-grained tasks such as authentication, logging and auditing, image and data compression, and functionality outside of Spring MVC.

HandlerInterceptor: Part of the Spring MVC framework, located between DispatcheerServlet and Controller. Suitable for cross-cutting concerns such as application logging, detailed authorization checking, and Spring context or model manipulation.

Image from [21]
Image from [21]

What is Polymorphism? [22]

Consequently, you can write programs that expect an object with a particular interface, knowing that any object that has the correct interface will accept the request. Moreover, dynamic binding lets you substitute objects that have identical interfaces for each other at run-time. This substitutability is known as polymorphism.

Memory location of Java primitive, reference type

Image from [23]
Image from [23]

@Transactional internal structure? [24]

So when you annotate a method with @Transactional, Spring dynamically creates a proxy that implements the same interface(s) as the class you're annotating. And when clients make calls into your object, the calls are intercepted and the behaviors injected via the proxy mechanism.

As you observed, though, the proxy mechanism only works when calls come in from some external object. When you make an internal call within the object, you're really making a call through this reference, which bypasses the proxy. There are ways of working around that problem, however. I explain one approach in this forum post in which I use a BeanFactoryPostProcessor to inject an instance of the proxy into "self-referencing" classes at runtime. I save this reference to a member variable called me. Then if I need to make internal calls that require a change in the transaction status of the thread, I direct the call through the proxy (e.g. me.someMethod().)

Java hashmap structure?

Implemented using Chaining with Hash Table [25]

C

If you were to implement Logging directly in Plain Java, how would you implement it?

It takes a long time to understand the intent of the question. I was advised to think more in terms of fundamental CS, regardless of the framework.

The approximate answer is, "Write to memory and write to a file periodically. Such processing will be implemented as an additional thread, and since Spring Controller is a singleton, it will be implemented by placing and retrieving a queue as a static member."

If you were to create an app that CRDs 64 bit numeric data, what data structure would you use and how would you implement it?

Instead of spitting out answers you have memorized, you are asked to explain each question at a level that can be implemented in detail. The hash table came to mind, but additional questions continued until I found the answer I wanted. I made a mistake while talking about what hash function to use, so I felt like I needed to study it in more detail [26].

Additional questions were asked about the hash table, and while discussing chaining and open addressing, etc.

  • What if there is skew and it becomes O(N)? -> I wanted to say that it would decrease with Universal hashing, but I thought it would be difficult to say accurately, so I passed.

  • When doubling tables, isn't doubling from 4GB to 8GB slow? -> I answered that I would increase it by 1.2 or 1.3 times as it grows little by little, but I felt that it was not the answer I wanted.

After going through many twists and turns, I remembered double hashing and (dynamic) perfect hashing and answered.

Comparison of Struts, DWR, Spring?

The main features can be compared in the table below [33, 34]:

| | Spring | Struts | DWR | | -- | --- | --- |--------------------------------------------- ---------------| | definition | Application framework used to implement IoC and DI | Framework used to extend the Java Servlet API and MVC framework | East Ajax for Java Java library for easy integration between Java and JavaScript | | Architecture | Layered Architecture | - | - | | framework | lightweight | heavy weight | heavy weight | | Link | ORM, JDBC technology, etc. | Integration is possible, but manual coding is required | Spring, Struts, Hibernate, etc. | | Coupling | loose | tight | tight |

Struts workflow - Image from [34]
Struts workflow - Image from [34]

Why did you change Struts to Spring?

Reduce complexity by using a consistent framework across the board, making it easier to test.

What were some of the technical challenges during refactoring?

When asking questions, I felt like what I was looking for was an example of solving a technical challenge, such as "solving a problem caused by a huge amount of traffic with excellent technology," but in reality, there was nothing like that, and there was a lot of history, unknown people in charge, and legacy that were difficult to understand, so I gave it a bit of a technical tone and went around in circles.

D

Differences between hash-based and tree-based implementations in Map [35, 36, 37]

Java HashMap extends the AbstractMap class and implements the Map interface. The Map implementation usually operates as a bucketed hash table, but when the buckets become too large, they are converted to TreeNodes nodes, making each structure similar to java.util.TreeMap.

TreeMap extends the AbstractMap class and implements the NavigableMap interface. TreeMap stores elements in a Red-Black tree (Self-Balancing Binary Search Tree).

HashMapTreeMap
inheritanceextends AbstractMap implement Mapextends AbstractMap implement NavigableMap
orderNot guaranteedGuaranteed
NullOne null key and multiple null values possibleNot nullable (because compareTo() throws NullPointerException)
Performanceadd(), remove(), and contains() are all O(1), but may cause memory overhead (when many buckets are not used) and performance degradation (when there are many collisions). Before Java 8, Chaining was used for collisions, but in JEP 180, when multiple collisions (when passing TREEIFY_THRESHOLD) occur, a structure similar to TreeMap was changed to reduce the worst performance O(n) to O(log n). box.Typically provides O(log n) performance for add(), remove(), and contains(). Memory can be saved compared to HashMap.

hash collision?

This refers to when the hash values of two different data are the same.

Resolution method [36]

  • Chaining
  • Open Addressing
    • Linear probing
    • Quadratic probing
  • Dynamic resizing
  • Double hashing

Difference between Java List and Stream

Streams differ from Collections in the following ways [37]:

  • No storage: A stream is not a data structure that stores elements. Instead, it functions to 'carry' data from sources such as data structures, arrays, generator functions, and I/O channels through the pipeline of computer operations.
  • Functional: Operations on streams produce results but do not change the source value.
  • Laziness-seeking: Stream operations such as filtering, mapping, and deduplication can be implemented lazily for optimization.
  • Possibly-unbounded: collections have a finite size, whereas streams do not. Short circuiting operations such as limit(n) or findFirst() allow computing on infinite streams to be processed in finite time.
  • Consumable: Elements of a stream can be visited only once while the stream exists. Like an Iterator, a new stream must be created to revisit the same element from the source.

How would you explain HDFS based on the CAP theorem? [38]

HDFS supports consistency and partition tolerance, but there is a trade-off in availability.

Consistency

The following content in the HDFS document clearly shows that HDFS is consistent:

"Create. Once the close() operation on an output stream writing a newly created file has completed, in-cluster operations querying the file metadata and contents MUST immediately see the file and its data." "Update. Once the close() operation on an output stream writing a newly created file has completed, in-cluster operations querying the file metadata and contents MUST immediately see the new data. "Delete. once a delete() operation on a path other than “/” has completed successfully, it MUST NOT be visible or accessible. Specifically, listStatus(), open() ,rename() and append() operations MUST fail."

Availability

The following content in the document indicates that it is not available. HA is provided, but when the name node is down due to SPOF, access to the file system is temporarily disabled.

"The time to complete an operation is undefined and may depend on the implementation and on the state of the system."

Partition Tolerance

Partition refers to communication interruption in a distributed system [39]. Even if one of the HDFS data nodes goes down or disappears, the system can operate without being affected.

Are you familiar with OAuth?

I said that I don't know well.

An open standard (authorization protocol) for access delegation. It is mainly used to allow Internet users to allow websites or applications to access their information on other websites without a password.

High-level overview of Oauth 2.0 flow - Image from [40]
High-level overview of Oauth 2.0 flow - Image from [40]

Have you used Spring Security much?

I said that I haven't used it much.

Provides authentication authorization to Java applications [41].

(Coding) Implementing List only with Java Array

[42]

If a message service is implemented using a database, what is the approximate ERD?

Image from [44]
Image from [44]
Image from [46]
Image from [46]
Image from [47]
Image from [47]
Image from [48]
Image from [48]
Image from [49]
Image from [49]

How would the above message service be implemented using Kafka?

Image from [43]
Image from [43]
Image from [50]
Image from [50]
Image from [51, 53]
Image from [51, 53]
Image from [52]
Image from [52]

Reference

[1] https://docs.oracle.com/javadb/10.8.3.0/ref/rrefsqlj29840.html

[2] https://kadensungbincho.com/blog/backend/apache-tomcat-internals

[3] https://tomcat.apache.org/tomcat-8.5-doc/config/http.html#Connector_Comparison

[4] https://stackoverflow.com/a/39089983/8854614

[5] http://www.allenlipeng47.com/blog/index.php/2020/01/05/timeouts/

[6] https://softwareengineering.stackexchange.com/a/70717

[7] https://kadensungbincho.tistory.com/58

[8] https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying

[9] https://aws.amazon.com/message-queue/benefits/

[10] https://kadensungbincho.tistory.com/98

[11] https://www.tutorialspoint.com/when-to-use-an-abstract-class-and-when-to-use-an-interface-in-java

[12] https://byexample.xyz/java/8/default/

[13] https://stackoverflow.com/a/479168/8854614

[14] https://stackoverflow.com/a/479154/8854614

[15] https://www.baeldung.com/aspectj

[16] https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop

[17] https://www.fatalerrors.org/a/proxy-pattern-and-concrete-implementation-of-spring-aop-programming.html

[18] https://realpython.com/python-gil/

[19] https://www.youtube.com/watch?v=Obt-vMVdM8s

[20] https://doc.pypy.org/en/latest/faq.html#does-pypy-have-a-gil-why

[21] https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter

[22] Design Patterns Elements of Reusable Object-Oriented Software

[23] https://stackoverflow.com/a/32049775/8854614

[24] https://stackoverflow.com/a/1099284/8854614

[25] https://www.geeksforgeeks.org/internal-working-of-hashmap-java/

[26] https://kadensungbincho.tistory.com/122

[27] https://stackoverflow.com/a/86021/8854614

[28] https://www.geeksforgeeks.org/mutex-vs-semaphore/

[29] https://blog.feabhas.com/2009/09/mutex-vs-semaphores-%E2%80%93-part-1-semaphores/

[30] https://barrgroup.com/embedded-systems/how-to/rtos-mutex-semaphore

[31] https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/How_the_Web_works

[32] https://kadensungbincho.tistory.com/70

[33] https://www.educba.com/spring-vs-struts/

[34] https://netbeans.apache.org/kb/docs/web/quickstart-webapps-struts.html

[35] https://www.baeldung.com/java-treemap-vs-hashmap

[36] https://stackoverflow.com/a/31161193/8854614

[37] https://stackoverflow.com/a/39432937/8854614

[38] https://stackoverflow.com/questions/58796173/how-does-the-cap-theorem-apply-on-hdfs

[39] https://www.ibm.com/cloud/learn/cap-theorem

[40] https://en.wikipedia.org/wiki/OAuth

[41] https://docs.spring.io/spring-security/site/docs/current/reference/html5/

[42] https://www.geeksforgeeks.org/internal-working-of-arraylist-in-java/

[43] https://tech.kakao.com/2020/06/08/websocket-part1/

[44] https://vertabelo.com/blog/database-model-for-a-messaging-system/

[45] https://dba.stackexchange.com/questions/268388/chat-conversation-history-entity-relationship-diagram

[46] https://dba.stackexchange.com/questions/268388/chat-conversation-history-entity-relationship-diagram

[47] https://stackoverflow.com/a/8351589/8854614

[48] https://stackoverflow.com/a/6542556/8854614

[49] https://stackoverflow.com/a/6425511/8854614

[50] https://engineering.linecorp.com/en/blog/the-architecture-behind-chatting-on-line-live/

[51] https://codetiburon.com/create-chat-app-like-whatsapp/

[52] https://engineering.linecorp.com/en/blog/line-storage-storing-billions-of-rows-in-sharded-redis-and-hbase/

[53] https://stackoverflow.com/a/29137325/8854614

Join our newsletter

Stay tuned with 100+ Software engineers
Latest backend & growth trends in your mail box on every Wednesday