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:
camelCasefor variables, methods, parametersPascalCase(UpperCamelCase) for classes, traits, objectsCONSTANT_CASEfor constants (final val)snake_casefor 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
getonly if there is a corresponding setter - Avoid single-letter names except for conventional ones (
i,j,kfor 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:
- package declaration
- Imports
- Class/trait/object
- Companion objects
- 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
valby default,varonly 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. UseOptionfor 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,filterinstead 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
Tryfor synchronous operations that may throw exceptions - Use
Eitherfor operations with domain-specific errors - Use
Futurefor 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
ClassNameSpecorClassNameTest - 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
nulland minimal use ofvar - 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)