Scala

1. General Principles

1.1 Purpose

This document describes Scala coding standards to ensure code consistency, readability, and maintainability. Following these standards helps reduce errors.

1.2 Consistency

  • In case of discrepancies between this standard and client standards, client standards take priority
  • Any deviation from the standard must be documented in comments with justification

2. Coding Style

2.1 Line Length

Rule: Line length should not exceed 120 characters

  • Break long expressions across multiple lines
  • Use appropriate break points (after operators, commas)

Example:

// Good
val result = longMethodName(parameter1, parameter2, parameter3) +
  anotherMethodCall() +
  yetAnotherCall()

// Bad
val result = longMethodName(parameter1, parameter2, parameter3) + anotherMethodCall() + yetAnotherCall()

2.2 Indentation

Rule:

  • Use 2 spaces for indentation. Tabs are prohibited.
  • Configure your editor accordingly

2.3 Braces

Rule:

  • Opening brace stays on the same line as the expression
  • Closing brace on a separate line

Examples:

// Good
if (condition) {
  doSomething()
}

try {
  operation()
} catch {
  case e: Exception => handleError(e)
} finally {
  cleanup()
}

// For single-line expressions without side effects, braces can be omitted
if (condition) doSomething()

// For multi-line or complex expressions - always use braces
if (condition) {
  val result = calculate()
  process(result)
}

2.4 Whitespace

Rule:

  • Operators are surrounded by spaces
  • Space after commas
  • No space before colon in type ascription

Example:

// Good
val sum = a + b
val list = List(1, 2, 3)
val x: Int = 42

// Bad
val sum=a+b
val list = List(1,2,3)
val x : Int = 42

3. Naming

3.1 Naming Styles

Rules:

  • camelCase for variables, methods, parameters
  • PascalCase (UpperCamelCase) for classes, traits, objects
  • CONSTANT_CASE for constants (final val)
  • snake_case for filenames and configuration keys

Examples:

// Classes and traits
class UserRepository
trait DatabaseService

// Methods and variables
def getUserById(id: Long): Future[Option[User]]
val maxRetryCount = 3

// Constants
final val DEFAULT_TIMEOUT = 30.seconds
final val DATABASE_URL = "jdbc:postgresql://localhost/db"

// Packages
package com.company.project.module

3.2 Method Names

Recommendations:

  • Use verbs for methods that perform actions
  • Use nouns for methods that return values
  • Getter methods start with get only if there is a corresponding setter
  • Avoid single-letter names except for conventional ones (i, j, k for indexes)

Examples:

// Good
def calculateTotal(): BigDecimal
def save(user: User): Future[User]
def isValid: Boolean
def user: User  // instead of getUser

// Bad
def totalCalculator(): BigDecimal
def userSaver(user: User): Future[User]
def checkIfValid: Boolean

4. Code Organization

4.1 File Structure

Rules:

  • One main class/trait/object per file
  • Filename should match the main class name (PascalCase)
  • Try to keep file size within 500-1000 lines

4.2 Declaration Order

Recommendations:

  1. package declaration
  2. Imports
  3. Class/trait/object
  4. Companion objects
  5. Nested classes

Example:

package com.company.service

import scala.concurrent.{Future, ExecutionContext}
import java.time.LocalDateTime

class UserService {
  // fields
  // methods
}

object UserService {
  // static methods
}

4.3 Imports

Rules:

  • Group imports: first Scala standard library, then Java, then third-party libraries, then own packages
  • Use curly braces for importing multiple elements from the same package
  • Avoid wildcard imports (import package._) except in tests

Example:

import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Try, Success, Failure}

import java.time.LocalDateTime
import java.util.concurrent.TimeUnit

import akka.actor.ActorSystem
import com.typesafe.scalalogging.LazyLogging

import model.User
import repository.UserRepository

5. Types and Variables

5.1 Variable Declaration

Rules:

  • Use val by default, var only when necessary
  • Explicitly specify types for public API
  • Local variables can use type inference

Examples:

// Good
val userName: String = "John"
val count = calculateCount()  // type inference acceptable for local variables

// Only when necessary
var retryCount = 0

// Bad
var temporaryValue = compute()

5.2 Null Safety

Rule:

  • Avoid using null. Use Option for nullable values.

Example:

// Good
def findUser(id: Long): Option[User]
val maybeUser: Option[User] = findUser(123)

// Bad
def findUser(id: Long): User = {
  // may return null
}

6. Functions and Methods

6.1 Method Signatures

Recommendations:

  • Limit number of parameters (maximum 5-7)
  • Use case classes for grouping related parameters
  • Explicitly specify return types for public methods

Example:

// Good
case class SearchCriteria(query: String, limit: Int, offset: Int)

def searchUsers(criteria: SearchCriteria): Future[Seq[User]]

// Bad
def searchUsers(query: String, limit: Int, offset: Int, 
                sortBy: String, ascending: Boolean, 
                includeInactive: Boolean): Future[Seq[User]]

6.2 Functional Style

Rules:

  • Prefer immutable collections
  • Use map, flatMap, filter instead of loops
  • Avoid side effects in pure functions

Example:

// Good
val activeUsers = users.filter(_.isActive).map(_.toDto)

// Bad
var activeUsers = List.empty[UserDto]
for (user <- users) {
  if (user.isActive) {
    activeUsers = activeUsers :+ user.toDto
  }
}

7. Error Handling

7.1 Using Try/Either/Future

Rules:

  • Use Try for synchronous operations that may throw exceptions
  • Use Either for operations with domain-specific errors
  • Use Future for asynchronous operations

Example:

// With Try
def parseNumber(s: String): Try[Int] = Try(s.toInt)

// With Either
def validateUser(user: User): Either[String, User] = {
  if (user.name.isEmpty) Left("Name cannot be empty")
  else Right(user)
}

// With Future
def fetchUser(id: Long): Future[User] = {
  // asynchronous operation
}

7.2 Exceptions

Rules:

  • Use specific exception types for domain errors
  • Log exceptions only at the level where they are handled
  • Preserve stack trace when wrapping exceptions

Example:

class UserNotFoundException(id: Long) 
  extends RuntimeException(s"User with id $id not found")

try {
  userRepository.findById(userId)
} catch {
  case e: UserNotFoundException => 
    logger.error(s"User not found: $userId", e)
    Left(ResponseStatus.UserNotFound)
  case NonFatal(e) =>
    logger.error("Unexpected error", e)
    Left(ResponseStatus.InternalServerError)
}

8. Comments

8.1 Documentation Comments

Rules:

  • Use Scaladoc for public API
  • Comment complex business logic
  • Avoid comments that duplicate code

Example:

/**
 * Repository for working with users.
 * 
 * @param db database connection
 * @param ec execution context
 */
class UserRepository(db: Database)(implicit ec: ExecutionContext) {

  /**
   * Finds user by identifier.
   * 
   * @param id user identifier
   * @return Future with optional user
   */
  def findById(id: Long): Future[Option[User]] = {
    // implementation
  }
}

8.2 TODO and FIXME

Recommendations:

  • Use standard tags: TODO, FIXME, NOTE
  • Include context and date

Example:

// TODO: Optimize query for large number of users (2024-01-15)
// FIXME: Handle edge case with empty search string
// NOTE: This method is used in reports, change with caution

9. Testing

9.1 Test Structure

Rules:

  • Test classes are named as ClassNameSpec or ClassNameTest
  • Use readable test names
  • Group related tests using describe/it

Example:

class UserServiceSpec extends AnyFlatSpec with Matchers {

  "UserService" should "return user by id" in {
    // test
  }

  it should "return None for non-existent user" in {
    // test
  }

  "save method" should "persist user to database" in {
    // test
  }
}

9.2 Mocking

Recommendations:

  • Use mocks only for external dependencies
  • Prefer real implementations where possible
  • Use dependency injection for testability

10. Performance and Optimization

10.1 Principles

Rule:

  • First write readable and correct code, optimize only when performance issues are proven.

10.2 Common Pitfalls

Avoid:

  • Excessive copying of large structures
  • N+1 database queries
  • Blocking operations in asynchronous context

11. Tools and Automation

11.1 Formatting

Use Scalafmt for automatic code formatting. Configuration .scalafmt.conf:

version = "3.7.14"
maxColumn = 120
continuationIndent.defnSite = 2
align.preset = some
rewrite.rules = [SortImports]

11.2 Static Analysis

Configure the following tools:

  • Scalafix for refactoring
  • WartRemover for checking best practices
  • Scapegoat for identifying potential issues

Appendix A: Examples

A.1 Good Practices from the Code

Using dependency injection:

class FuelGaugesRoutes @Inject()(
  system: ActorSystem,
  userRepository: UserRepository,
  fuelGaugeRepository: FuelGaugeRepository,
  @Named("restActor") apiActor: ActorRef,
  config: Config
)(implicit @Named("global") executionContext: ExecutionContext)

Using type classes:

// Type class  as trait with parameter
trait JsonWriter[A] {
  def write(value: A): Json
}

// simple introduction JSON
sealed trait Json
case class JsString(value: String) extends Json
case class JsNumber(value: Double) extends Json
case class JsObject(fields: Map[String, Json]) extends Json

object JsonWriterInstances {
  // Instance for String
  implicit val stringWriter: JsonWriter[String] = 
    (value: String) => JsString(value)

  // Instance for Int
  implicit val intWriter: JsonWriter[Int] = 
    (value: Int) => JsNumber(value.toDouble)

  // Instance for Person
  case class Person(name: String, age: Int)

  implicit val personWriter: JsonWriter[Person] = 
    (person: Person) => JsObject(Map(
      "name" -> JsString(person.name),
      "age" -> JsNumber(person.age)
    ))

  // Instance for Option (recursion instance)
  implicit def optionWriter[A](implicit writer: JsonWriter[A]): JsonWriter[Option[A]] =
    (option: Option[A]) => option match {
      case Some(value) => writer.write(value)
      case None => JsString("null")
    }
}

object Json {
  def toJson[A](value: A)(implicit writer: JsonWriter[A]): Json =
    writer.write(value)
}

// alternative syntax with context bound
object JsonSyntax {
  implicit class JsonWriterOps[A](value: A) {
    def toJson(implicit writer: JsonWriter[A]): Json =
      writer.write(value)
  }
}

Structuring configuration:

lazy val commonSettings = Seq[Def.SettingsDefinition](
  scalaVersion := "2.13.16",
  version := releaseVersion,
  organization := "dataroot"
)

A.2 Areas for Improvement

Avoid overly long lines:

// Instead of:
val all = endpoint(request(Get, root /? (qs[Option[String]]("query") & qs[String]("sort_by") & qs[String]("asc") & qs[Int]("pageNumber") & qs[Int]("rowsNumber")  /*& optQs [String]("filterParams")*/)), ok(jsonResponse[ResponseStatus Either GetResponse]))

// Break into:
val all = endpoint(
  request(
    Get, 
    root /? (qs[Option[String]]("query") & 
             qs[String]("sort_by") & 
             qs[String]("asc") & 
             qs[Int]("pageNumber") & 
             qs[Int]("rowsNumber") /*& optQs[String]("filterParams")*/)
  ),
  ok(jsonResponse[ResponseStatus Either GetResponse])
)

Use explicit return types for public API:

// Instead of:
def findAll2(user: User, parameters: GetParameters) = {
  // implementation
}

// Use:
def findAll2(user: User, parameters: GetParameters): Future[ResponseStatus Either GetResponse] = {
  // implementation
}

Appendix B: Code Review Checklist

  • Compliance with naming standards
  • Line length does not exceed 120 characters
  • Correct indentation (2 spaces)
  • Explicit types for public API
  • No null and minimal use of var
  • Error handling via Try/Either/Future
  • Tests for new functionality
  • No commented-out code
  • Current TODO/FIXME with context
  • Correct Scaladoc comments for public API
  • Compliance with functional programming principles where appropriate
  • No blocking operations in asynchronous context
  • Efficient use of collections (without excessive copying)