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)
Document - Scala