The following is a somewhat opinionated style guide targeted at people primarily with Java background who want to benefit from using the mainstream (also known as
Future-based) Scala stack and do so while optimizing their reward to effort ratio. This guide will be well suited for teams that need a reasonable set of simple guidelines to follow, especially at the beginning, until everyone develops good intuition for Scala.
Signal your intention with crystal clear names
When it comes to programming, it's better to be wrong than vague. This implies that you should document your intent in code as clear as possible. For example, if you have a choice between calling a variable
documentId and you know that you're dealing with documents here, choose the latter. Generic names such as
correlationId are OK in generic code, but using them in business logic is usually a bad idea. Likewise, avoid using data types as names, i.e it's better to use
created rather than
Minimize the LOC measure by leaning towards longer lines
This is somewhat controversial, but when it comes to reading the code, I much prefer something that doesn't spread multiple screens. Of course, this doesn't mean that you should try to fit everything into a single line. Rather, try to group operations so that they make up a single logical action. For example, if you have a collection of elements and you need to get something from it through a sequence of method calls, feel free to keep them on the same line. If the level of abstraction throughout the method as consistent as it should be, every line can be viewed as a single finished thought.
Strive for methods that mostly consist of single line val assignments
If a method consists of multiple
val assignments, it forms a very easy to follow structure. Provided that you named your values and methods reasonably well, your Scala code should read like English. Occasional branching is OK, but it shouldn't distract the reader from the main idea, and the main flow should be easily evident.
Keep nesting to a reasonable level
Scala offers great many tools to avoid excessive nesting. Rather than building a pyramid of
flatMaps, we can always write a
for-expression instead. Instead of writing multiple nested
if-expressions, we can wrap everything in
Try and then use a Failure with an application-specific exception to signal an erroneous condition. If we're not interested in distinguishing between different erroneous conditions, we, yet again, can use a
Always keep class members minimally exposed
By default, always prefer the
private access and only resort to
public (which is default in Scala) if you must. This will make it much easier for a reader to understand the code in order to change it. Fight the temptation to move functionality that might be useful somewhere else to the
commons module where it will be available to every service: most of the time this move will be premature, require serious refactoring later and result in a lot of unnecessary changes rippling throughout the code base.
Do not keep mutable state in service classes
Most service classes are stateless collections of methods grouped together, because they perform related business functions and probably have similar or overlapping dependencies. Because they are stateless, they can safely be instantiated as
@Singletons without any concern about concurrency. This is exactly the opposite of what classical object-oriented programming tells us to do, and this is perfectly fine as we're not doing OOP here.
Avoid putting logic in case classes
Whereas service classes should contain only methods but not state, case classes should contain state but not logic. Again, this is in direct contrast to what OOP tells us to do. Also, keep in mind that if the case class in question resides in the
commons package, everything that was said about keeping exposure to a minimum is still relevant here.
Always destructure tuples
It's easy enough to create a tuple, but it's not always easy to read the code that is full of them. If you're calling
zipWithIndex, for example, it's better to destructure the pair immediately thereby avoiding cryptic calls to
._2. Likewise, never return a tuple from a method, especially
public. If the need arises, use a properly named case class (possibly, defined within a service class to limit its scope).
Use for expressions for monadic sequencing only
for expressions can be used to express methods such as
foreach, so in a way they behave like a mixture of the
do notation from Haskell and list comprehensions from Python. In practice,
foreach are better left unsugared, especially when collections are involved. On the other hand, sequentializing monadic types like
IO is a great way to make code more readable and reduce nesting.
Use val whenever possible and only resort to var if necessary
Unlike Haskell, Scala has the
var keyword and allows developers to define mutable variables. However, in an expression-centric language they are almost never needed. For the most part, you should only consider using a method-local
var in a very rare situation of performance optimization. Everything else is better done with
Consider replacing while loops with @tailrec functions
Most iterations in Scala can be expressed in terms of library functions. However, sometimes there is a need to exit the iteration urgently and still return a result. This can be done either imperatively with
var and breaks or with recursion. In many cases, recursion results in a more understandable code, so take time and try to implement it, but make sure that it can be annotated with
Prefer enumeratum to the standard Enumeration
The standard pattern for implementing
enum in Scala is described on StackOverflow and often used by Scala beginners. This approach is very limited and in fact inferior to the standard Java
enum in almost every way. Always prefer enumeratum as a way of implementing enums until Scala 3 is released, and only resort to using
Enumeration to support legacy systems.
Consider using union enums with Either to signal expected errors
Java has the notion of checked exceptions, which is mostly viewed by the Java community as a design mistake. However, this concept allows developers to differentiate between expected and runtime errors. Even though all exceptions in Scala are essentially
RuntimeException's, in many situations it is necessary to make expected errors part of the API. The best strategy here is to use
Either with a custom error type as
Left value and normal return value as
Right. The custom error type should ideally be an algebraic data type, i.e should consist of non-intersecting concrete values. Passing
Strings with an arbitrary error message in English as
Left is almost always a bad idea.
Use Option only when the absence of value is expected
If properly used,
Option completely eliminates the problem of
NullPointerException in Scala code. Ideally, it should be used to signal to the caller that the value may or may not be present. Since
Option is effectively part of the API, it forces the caller to make sure that both possibilities are covered. For example,
findById methods of a low-level repository might return an
Option to signal that the value might not be present in the database. However, if the entity is expected to exist for some other higher-level operation, its absence should be signalled with a failure.
Be mindful of where your Future is running
Most methods on
Future require an
ExecutionContext that specifies in which thread pool this particular piece of code is going to run. Often times, people simply define or import a global
implicit value and forget about the problem altogether. Just as often this creates a situation when all code, including blocking and long-lasting operations, is running on a single CPU thread pool depriving other tasks from live threads.
Avoid accidental exception swallowing
Many monadic types such as
Try catch exceptions internally, and most of the time, this is exactly what you want. However, it's also very easy to accidentally swallow an exception completely thereby depriving the caller from ever knowing that an error took place. Always be careful with error propagation when using methods like
recover and be doubly suspicious when you encounter a nested
Keep inheritance use to an absolute minimum
Using inheritance to avoid code duplication is a terrible idea in Java, and it's no different in Scala. If there's some logic that can be expressed as a pure function and reused by other services from the same module, it's better to extract it as a helper method. Likewise, if the functionality of a certain class needs to be extended, use composition as GoF suggested in "Design Patterns" and Josh Bloch re-asserted in "Effective Java".
Avoid unnecessary method overloading
Method overloading in Scala works exactly the same way as in Java, but this doesn't mean that it should be used just as often. While Java has to distinguish between primitives and reference types, Scala doesn't have this problem, and overloading is usually a bad idea as it makes code less readable. And there are many other reasons to avoid overloading in Java and in Scala.