Backend Engineering
- Monolith vs Microservices Architecture
- Service to Service communication
- Scaling
- Monitoring
- Functional Programming
- Asynchronous Programming
- Thread
- CompletableFuture
- Apollo, gRPC, Hermes
- Testing
- Monitoring
- Deployment
- gRPC
Monolith vs Microservices Architecture
- Monolith: User Interface + Business Logic + Data Access Layer in a single code base
- Microservices: Split up Monolith, have a dedicated system and databse for each business logic
- Benefits: independently scale a system which is heavily used
Service to Service communication
- HTTP
- Hermes: more high performance than HTTP
- gRPC: remote procedure call
Scaling
- Vertical scaling (bigger machines)
- Horizontal scaling (more machine)
- Virtual Machines: virtualizes the hardware
- Kubenetes: virtualizes the operating system
Monitoring
- Logging: e.g. Google Cloud Logging
- Metrics: time-series data showing how things changed, e.g. Grafana (dashboard), pager duty (alerting)
- Distributed request tracing: shows how a request goes through different systems/services, how long does it take, e.g. Lightstep
Functional Programming
- Everything is immutable, avoids mutability
- Functional Interfaces
- Lambda Expressions
- Method Reference
- Optionals
- Streams
Functional Interfaces
- An interface with exactly one abstract method
- Can be implemented by Lamdba Expressions or Method Reference
- Example: Predicate
public interface predicate<T> {
boolean test(T t);
default Predicate<T> negate() {
return (t) -> !test(t);
}
... other non-abstract methods
}
Lambda Expressions
- Syntactic sugar
- Instances of Functional Interfaces
- Implements the only method in the Functional Interface
- Implemented inline, as a variable or as in put to a method like a stream. The compiler infers the underlying Functional Interface using the type of the variable or parameter.
- Benefits: more readable, type safety
// Syntax
// 1st style
(p1, p2) -> doSomething(p1, p2);
// 2nd style: curly braces for multi statement method body or return statement
(parameters) -> {
return doSomething(parameters);
}
// Example 1
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 1st style
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
new Thread(runnable).start();
// 2nd style
Runnable runnable = () -> System.out.println("Hello");
new Thread(runnable).start();
// Example 2
// 1st style
Comparator<Integer> c =
new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b;
}
};
// 2nd style
Comparator<Integer> c = (a, b) -> a - b;
Function Shapes
- built-in in
java.util.function
- Consumer: takes one param of specified type, returns nothing
Consumer<Integer> printer = i -> System.out.println(i);
- Function: takes one param of specified type, returns the specified type
Function<Integer, Integer> twice = i -> i * 2;
- Supplier: takes no parameters, returns the specified type
Supplier<Long> currentTime = () -> Instant.now().toEpochMilli();
- Predicate: takes one param of specified type, returns boolean
Predicate<Integer> greaterThanTen = num -> num > 10;
Method Reference
- shorthand for special type of Lambda expressions
- Opt/Alt + Enter in IntelliJ can convert a lambda expression to a method reference
// Lambda
n -> Adder.addOneTo(n);
// Method Reference
Adder:addOneTo;
// Lambda
Adder adder = new Adder(10);
n -> adder.add(n);
// Method Reference
adder::add;
// Lambda
n -> new Adder(n);
// Method Reference
Adder::new;
Optionals
- immutable container used to contain a non-null value of any Object type
- benefits: avoids NPEs and
if (x == null)
Optional<Integer> v1 = Optional.of(5); // Optional[5]
Optional<Integer> v2 = Optional.empty(); // empty Optional
Optional<Integer> v3 = Optional.of(null); // throws NullPointerException
Optional<Integer> v4 = Optional.ofNullable(null); // empty Optional
Optional<Integer> v5 = Optional.ofNullable(x); // nullable -> Optional
- Example
class UserData {
public String name;
public String product;
public Optional<String> email;
}
interface UserStore {
// Return Optional with user if userId is known, otherwise Optional.empty()
Optional<UserData> lookup(final String userId);
}
class InMemoryUserStore implements UserStore {
public InMemoryUserStore(Map<String, UserData> users) {
this.users = users;
}
public Optional<UserData> lookup(String userId) {
return Optional.ofNullable(users.get(userId));
}
}
users.lookup(userId)
.map(user -> user.name)
.orElse("");
users.lookup(userId)
.flatMap(user -> user.email)
.orElse(null);
- Unwrapping an Optional
get()
: returns the contents if present, otherwise throws an exception should be avoided, anti-patternorElse(fallbackValue)
orElseGet(() -> fallbackComputation())
: requires a SupplierorElseThrow(() -> generateException())
isPresent()
: using optional imperatively, better option -ifPresent(v -> useValue(v))
Stream
- a sequence of objects which supports various methods taht can be pipelined together to get the desired result
- not a data structure, but can take input from different data structures
- immutable
- benefit: avoids explicit for/while-loops
list.stream() // generator
.map(e -> e.name()) // operation: not applied straight awai
.filter(n -> !n.isEmpty()) // operation
.findFirst() // terminator, returns Optional<T>
- Generators: used to feed data into the streams
Stream.empty()
: Empty streamStream.of(1,2,3)
: Stream of three elementslist.stream()
: Stream from a List, can be used on any Java Collectionmap.entrySet().stream()
: Stream from the entries in a map
- A stream only be consumed once
Stream<Integer> nStream = Stream.of(1,2,3);
nStream.map(x -> x * 2).forEach(System.out::println);
nStream.map(x -> x * 3).forEach(System.out::println); // throws IllegalSstateException (nStream is closed)
- Collectors:
toList()
,toSet()
,toCollection()
numbers.collect(Collectors.toCollection(LinkedList::new));
Collectors.toMap(keyMapper, valueMapper, mergeFunction)
:mergeFunction
: is used if there are conflicting keys in the map.
numbers
.collect(Collectors.toMap(
Function.identity(),
i -> i * 100,
(oldV, newV) -> newV // use new value in case of conflict
));
groupingBy(keyExtractor)
: groups a stream into a Map based on a given function
Map<String, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Asynchronous Programming
- Pros: Allows multiple things to happen at the same time. It doesn't block the execution flow
- Service-Oriented Architecture (SOA): each service will have to talk to other systems to reach its objective
- Each request is handled on a separate "thread"
- Cons: Codes get convoluted. Shared state and mutable objects should be avoided/used in a "thread-safe" manner
Thread
- A unit of execution within a process
- Each thread runs a path of execution through the program
- When starting an application, execution begins on the Main Trhead
- Additional trheads may be spun off to execute code in parallel to the main thread
- e.g. The main thread gets a list of files on the file system, then spins off 10 threads to each process 1/10th of the files
- The program will exit when the Main Thread commpletes execution
- Must explicitly tell the Main Thread to wait for the completion of the "child" threads
class MyCustomThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("From Thread: " + i);
}
}
}
}
MyCustomThread thread = new MyCustomThread();
thread.start();
class Printer extends Thread {
private int id;
public Printer(int id) { this.id = id; }
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("[Thread " + id + "]: " + i);
}
}
}
Printer printer1 = new Printer(1);
Printer printer2 = new Printer(2);
printer1.start();
printer2.start();
printer1.join();
printer2.join();
- Thread vvs Runnable
- Implementing Runnable interface is generally considered to be better
- Runnable favors composition over inheritance
- Runnable allows execution to be handled by an ExecutorService
- Java doesn't support multiple inheritance. Runnable alllos to extend fro ma superclass besides Thread
Thread Pool
- a way to reuse previously created threads in order to execute tasks
- certain predefined limits are established (e.g. number of threads) prior to the creation of the pool
// Threadpool with 1 Thread
Executor e1 = Executors.newSingleThreadExecutor()
Executor e2 = Executors.newFixedThreadPool(10)
ExecutorService
- An interface designed to allow you to easily submit tasks which need to be executed
- Contains methods for controlling the progress of the tasks which are submitted to it
- Allows a Runnable or a Callable to be submitted and executed asyncchronously on a Threadpool
- Maintains a blocking queue which can take requests and execute them as resources become available
ExecutorService ex = Executors.newSingleThreadExecutor();
ex.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("From Runnable: " + i);
}
});
Callable & Future
- Runnable interface cannot produce a result
- Callable interface allows you to execute async code, but also provides utilities to return a result
- The result returned by Callable is a Future
- It can also throw exceptions fro the block of computation
Future
- Represents the result of an async computation
- Provides methods to retrieve result from an async computation and also to check its status
- Methods offered by the Future Interface:
get()
,get(timeout, timeUnit)
,isDone()
,cancel()
,isCancelled()
get()
: try to avoid using it. Waits indefinitely for the result of the async computationget(timeout, timeUnit)
: similar toget()
, but only waits for a specific time
Callable<Integer> c =
() -> IntStream
.range(1, 1000)
.reduce()
.getAsInt();
ExecutorService ex = Executors.newSingleThreadExecutor();
Future<Integer> f = ex.submit(c);
System.out.println("Is it done: " + f.isDone()); // could be True or False
System.out.println("Result: " + f.get()); // will wait for the result of the async computation
System.out.println("Is it done: " + f.isDone()); // will always be True
- Is old and busted
- CompletableFuture is new
- Won't deal with Future or Executors directly. Will instead interact with libraries returning them
CompletableFuture
- Implements the Future and CompletionStage interfaces
- Allows you to create complex chains of async events
- Functions should return a CompletionStage (the interface)
- CompletableFuture class has utilities for easily returning objects through CompletionStage interface
- Chained CompletionStages allow us to implement series of async tasks which depend on the results of one another
- thenApply: takes the result of a CompletionStage, then returns its own result
public CompletionStage<List<String>> getArtistGenres(final String artistId) {
CompletionStage<Artist> artistFuture = metadataClient.getArtist(artistId);
return artistFuture.thenApply(artistMetadata -> {
reutrn artistMetadata.genres();
});
}
- thenComposeAsync: takes the result of a CompletionStage, then returns another CompletionStage whcih gets unnestest into the parent
public CompletionStage<Map<String, String>> getArtistGenresWithDetails(final String artistId) {
CompletionStage<Artist> artistGenresFuture = getArtistGenres(artistId);
return artistGenresFuture.thenComposeAsync(artistGenreList -> {
// getGenreDetails returns CompletionStage<Map<String, String>>
reutrn genreClient.getGenreDetails(artistGenreList);
});
}
- thenCompose: chain CompletionStage within thenCompose
public CompletionStage<Map<String, String>> getArtistGenresWithDetails(final String artistId) {
CompletionStage<Artist> artistGenresFuture = getArtistGenres(artistId);
return artistGenresFuture.thenCompose(artistGenreList -> {
// getGenreDetails returns CompletionStage<Map<String, String>>
return genreClient.getGenreDetails(artistGenreList)
.thenApply(genreDetails -> {
return genreDetails.entrySet().stream()
.map()
.collect();
});
});
}
-
thenCombine: execute two CompletionStages in parallel and take action on the result of both futures when they complete
-
thenAccept: consumes the result of a CompletionStage, but doesn't return anything
-
supplyAsync/completedFuture: create a CompletableFuture from a synchronous object/task
}); }
-
.exceptionally(): Exception Handling
return artistFuture.thenApply()
.exceptionally(e -> {
LOG.error(e);
return List.of(); // returns an empty list
});