Reinvent the Wheel, not to produce more Wheels but to make more Inventors
It is difficult to aggregate “collective wisdom from the experts” for things with ever-changing landscape, like Java. Either the advice veers strongly toward “my opinion from that experience 7 years ago” (stale) or generally accepted good practice, often obvious, like “encapsulate everything”. This is a relatively weaker book in the series. The “actionable idea density” is fairly low. About 15 of the items are generic hand waving, a similar number on low level constructs (coroutines in Kotlin, record, GC etc) and a handful meet the high standard of codifying in a book. Relatively few “aha” insights moments.
Some ideas, opinions and tips from the book -
- As apps move to serverless architectures, where the deployment units can be single functions, the benefits we get from application frameworks diminish.
- “We have to reinvent the wheel every once in a while, not because we need a lot of wheels; but because we need a lot of inventors”
- Ergonomics of older JVMs are fooled when running inside docker containers.
- Containers’ key benefit — reduce the coupling between an app and the underlying platform.
- JVM ergonomics enables JVM to tune itself by looking at two env metrics — (i) number of CPUs and (ii) available memory.
- Behavior is easy, state is hard — offer only a carefully designed API surface for any state mutation. Hardest bugs are caused by inconsistent state.
- JMH has one of the best profiling annotator languages (above) — most importantly, an overwhelming number of tools report statistics assuming normal distribution. In reality, it often is not normal.
- One of the advantages of being a job hopper is that “I’ve seen the way many different teams work”.
- Developer Productivity Engineering — practice and philosophy of improving developer experience through data.
- Clojure implements concurrency by turning the heap into a transactional data set (Software Transactional Memory). This is inefficient in massively parallel systems where concurrent writes are more likely. I.e., retries will become increasingly costly and performance unpredictable. Scala implements concurrency with message flow between actors. Queues are good for unidirectional communication but introduce latency.
- Actor model reduces the consequences of mutable state by preventing sharing, and functional programming makes the state shareable by prohibiting mutation. Functional programs can be less efficient than imperative equivalents and may place a bigger burden on GC. Lambdas facilitate the use of reactive programming paradigm consisting in asynchronous processing of stream of events.
- CountDownLatch is a powerful tool but blocks the current thread. Different concurrency models don’t play well together.
- Imperative — code explicitly telling the computer what to do. Declarative — code expressing a goal abstracting over the way in which the goal is achieved.
- You aren’t paid to write code. Change your focus from writing code to delivering software. Your job is designing change, not code. Code is a detail. Designing change means feature flags
- Nodatime.org (in .NET) and java.time library are similar. Three main concepts — LocalDateTime, ZonedDateTime and Instant. Schedule an event at a particular point in time in future — use ZonedDateTime (DST/UTC may change in between); recurring events with duration — either fix start OR end, and use duration to adjust.
- Localizing the scope of anything that is variable into a supporting method makes the top-level code predictable. Broadly, trying to lock down anything that does not vary makes you think more carefully about design and flushes out potential bugs.
- Embrace SQL thinking — the biggest failing of applied OO programming is the belief that you should faithfully reproduce your domain model in code. It leads to unnecessary classes. SQL is declarative and is a data DSL.
- In the early 2000s, most Java developers were actually full-stack developers (Spring, Struts, EJB, JSP/Servlets) — they just did not know because the phrase had not yet been coined!
- Heap access is geologically slow compared to instruction processing, so modern computers use caches.
- Java profilers use either byte code instrumentation or sampling. Flame graph sorts and aggregates the traces up to each stack level. Each block (rectangle) — width represents the percentage and “flames” represent from bottom to top the progression from entry point of the program or thread (main or an event loop). Only the relative widths and stack depths are relevant. If you have a flame that’s “very wide” on top, it may be a bottleneck not delegating work elsewhere. AyncGetCallTrace allows gathering of stack traces outside of safepoints, and combining measurement of JVM operations with native code and system calls to OS so an entirety (including IO, Network, GC etc) becomes part of the flame graph as well. Async-Profiler is a good tool.
Uncertainty is when we cannot quantify the risk.
Risk = (Likelihood of Failure X Worst-case impact of Failure)
- Playing the lottery is low-risk, Free solo climbing is high risk.
Large, infrequent releases are riskier. There is no way to reduce the impact of a failure — the worst case is still the ENTIRE system down and incur severe data loss, but we lower the likelihood of failure with smaller releases.
Targeted Journey Line —
From Uncertainty (no tests, no way to know shit)
to High Risk (High Likelihood) from large, infrequent releases
to Low Risk (Low Likelihood) from small, frequent releases
Our target end state is Low Risk deployment knowing we cannot do anything about Worst-case impact (as the popular saying goes — “in the long run, we are all dead”).
- Naming is important — Orwell said — “the worst thing one can do with words is surrender to them”.
- Technical classes (e.g., Hashmap) rarely have a place in the vocabulary of the domains we’re working in. The need for utility classes should be a red flag that you’re missing an abstraction.
- Avoid returning Null — returning an optional<T> is a better option that makes the code more explicit. For an operation that can have an optional parameter, create two methods — one with the param and one without.
- Try to crash your JVM to learn deep stuff — e.g., try use Unsafe to get access to low-level stuff (e.g., memory management) — syntax of C, safety of C; try writing Native code — syntax AND safety of C; try creating a class at runtime that only calls System.exit, and load that dynamically via classloader — call it; try to exhaust threads or file limits of your OS; modify your own .class files; kill your PID by using Runtime.exec; run JVM with -noverify to escape the safety net (disables all bytecode verification) etc.
- Cost of going to RAM, relative to registers, kept growing.Thus, cache-friendly code is a much researched area. Object types that can be stored directly into an array are called “value types” or “inline types” (e.g., int and char) and will soon have user defined ones, probably called “inline classes”. Inline types do not need to be allocated on the heap, and can be stored objects in your stack frame or directly in registers.
- Fun can have different faces — Exploration (focused investigation); Play (no goal, for its own sake); Puzzles (rules with a goal); Games (rules with a winner) and Work (satisfying goal).
- What is the type of null and why cannot it be assigned to a var? Null type is a special type — the type of expression null, which has no name, Because the type has no name, it is impossible to declare a variable of null type or to cast to the null type. These types are called “unspeakable types” or “nondenotable types”.
- Flatmap is applicable for anything that is a “container of something” — e.g., Stream<T> and CompletableFuture<T>
- Kotlin offers better thread-safety and null safety with reduced boilerplate. A ? operator follows any nullable reference in Kotline. The “?” stops evaluating automatically and returns null as soon as any of the referents in the chain resolves to null. Kotlin coroutines are like goroutines. It prevents monopolizing of threads.
- Kintsugi is a Japanese art where a precious broken object — rather than thrown away — is put back together using gold powder along its cracking lines. We should similarly learn how to fix legacy code that should make us proud.
- Optional.of throws a NPE when invoked with null, something unexpected from a class designed to prevent NPEs! It breaks the principle of least astonishment.
- All a constructor does is assign values to fields. Its only job is to create a valid instance. If there is more work to do, use a factory method.
- Package-by-layer structure (e.g., tld.domain.project.controllers, ..model, ..service etc) for your classes requires a lot of methods to be public. In order to benefit from package-private access protection, package-by-feature hierarchy is a better organizing principle (e.g., tld.domain.project.company, ..user etc). The latter reduces coupling.
- Service hedging — multiple idempotent calls to identically configured service instances on discrete nodes are launched and all but the fastest response discarded.
- Boolean literals are worse than hardcoded numbers — a 42 in code might look familiar, but a “false” could be anything and anything could be false. E.g., A Boolean available field is an optional “available” value — “not available” really may mean “out of stock”. Using null to mean something is the worst possible way to implement a third value. Refactor Boolean fields to a Java enum when possible. Domain models often suffer from primitive obsession. In the domain language, Boolean types are false and enumerated types are the truth.
- Classes that represent Value objects don’t need getters or setters. Why Java 14 introduced a record structure.
- TDD is very simple — Red, Green, Refactor. Red — focus on behavioral intent of code. I.e., only on the public interface. Green — do the simplest thing that makes the test pass. Refactor — make small simple steps, and rerun the tests to confirm everything still works.
- Great tools in JDK’s bin/ directory — many can be also run against remote JVMs.
jps -v : shows all arguments passed to the JVM
javap — c <class file> : shows complete bytecode
jmap -histo <pid>: prints histogram of each class in the heap with memory consumed
jmap -dump:format=b, file=<name> <pid>: dumps a snapshot of heap
jhat <heap dump file> : reads the heap dump file locally
jinfo <pid> : shows all system properties of JVM
jstack <pid> : takes stack traces of all threads running
jconsole & jvisualvm : graphical tools doing everything above
jshell: REPL tool
- Marginality : what is the average diminishing marginal return of adding developers to a team?
- C# does not have checked exceptions — they consider it obstacles.
- Programmers writing automated tests suffer from positive test bias. Fuzz testing is an unreasonably effective technique for negative testing that is easy to include in automated test suites. https://github.com/npryce/snodge is a good such tool.
- Comment only what the code cannot say. Code says “what”, comments should say “why”.