Packages and Imports: Organizing Your Code
Introduction
As your Scala applications grow, organizing code becomes crucial for maintainability, collaboration, and modularity. Packages provide a way to group related classes and objects into namespaces, while imports allow you to use code from different packages. Understanding how to structure packages and manage imports effectively is essential for building real-world Scala applications.
This lesson will teach you how to create well-organized package structures, use imports efficiently, manage visibility, and create modular architectures that scale with your application's complexity.
Package Declaration and Structure
Basic Package Declaration
// File: src/main/scala/com/company/myapp/models/User.scala
package com.company.myapp.models
case class User(id: Long, username: String, email: String, isActive: Boolean = true) {
def displayName: String = s"$username ($email)"
}
object User {
def create(username: String, email: String): Either[String, User] = {
if (username.length < 3) {
Left("Username must be at least 3 characters")
} else if (!email.contains("@")) {
Left("Invalid email format")
} else {
Right(User(System.currentTimeMillis(), username, email))
}
}
}
// File: src/main/scala/com/company/myapp/models/Product.scala
package com.company.myapp.models
case class Product(
id: String,
name: String,
price: BigDecimal,
category: String,
inStock: Boolean = true
) {
def isExpensive: Boolean = price > 100
def formattedPrice: String = f"$$${price}%.2f"
}
// File: src/main/scala/com/company/myapp/services/UserService.scala
package com.company.myapp.services
import com.company.myapp.models.User
class UserService {
private var users: Map[Long, User] = Map.empty
def createUser(username: String, email: String): Either[String, User] = {
User.create(username, email) match {
case Right(user) =>
users = users + (user.id -> user)
Right(user)
case Left(error) => Left(error)
}
}
def findUser(id: Long): Option[User] = users.get(id)
def findByUsername(username: String): Option[User] = {
users.values.find(_.username == username)
}
def listActiveUsers(): List[User] = {
users.values.filter(_.isActive).toList
}
}
Nested Packages
// File: src/main/scala/com/company/myapp/data/repositories/UserRepository.scala
package com.company.myapp.data.repositories
import com.company.myapp.models.User
trait UserRepository {
def save(user: User): Either[String, User]
def findById(id: Long): Option[User]
def findByEmail(email: String): Option[User]
def delete(id: Long): Boolean
def list(): List[User]
}
// File: src/main/scala/com/company/myapp/data/repositories/impl/InMemoryUserRepository.scala
package com.company.myapp.data.repositories.impl
import com.company.myapp.data.repositories.UserRepository
import com.company.myapp.models.User
class InMemoryUserRepository extends UserRepository {
private var users: Map[Long, User] = Map.empty
def save(user: User): Either[String, User] = {
users = users + (user.id -> user)
Right(user)
}
def findById(id: Long): Option[User] = users.get(id)
def findByEmail(email: String): Option[User] = {
users.values.find(_.email == email)
}
def delete(id: Long): Boolean = {
if (users.contains(id)) {
users = users - id
true
} else false
}
def list(): List[User] = users.values.toList
}
// File: src/main/scala/com/company/myapp/data/repositories/impl/DatabaseUserRepository.scala
package com.company.myapp.data.repositories.impl
import com.company.myapp.data.repositories.UserRepository
import com.company.myapp.models.User
class DatabaseUserRepository extends UserRepository {
def save(user: User): Either[String, User] = {
// Database save logic
println(s"Saving user to database: ${user.username}")
Right(user)
}
def findById(id: Long): Option[User] = {
// Database query logic
println(s"Querying database for user ID: $id")
None // Simplified for example
}
def findByEmail(email: String): Option[User] = {
// Database query logic
println(s"Querying database for user email: $email")
None // Simplified for example
}
def delete(id: Long): Boolean = {
// Database delete logic
println(s"Deleting user from database: $id")
true
}
def list(): List[User] = {
// Database list query
println("Listing all users from database")
List.empty // Simplified for example
}
}
Import Statements
Basic Imports
// File: src/main/scala/com/company/myapp/controllers/UserController.scala
package com.company.myapp.controllers
// Import specific class
import com.company.myapp.models.User
// Import multiple classes from same package
import com.company.myapp.services.{UserService, ProductService}
// Import all classes from a package
import com.company.myapp.data.repositories._
// Import with alias
import com.company.myapp.data.repositories.impl.{InMemoryUserRepository => MemoryRepo}
// Import Java classes
import java.time.LocalDateTime
import java.util.{UUID, Optional}
// Import Scala collections
import scala.collection.mutable
import scala.util.{Try, Success, Failure}
class UserController {
private val userService = new UserService()
private val userRepo: UserRepository = new MemoryRepo() // Using alias
def createUser(username: String, email: String): Either[String, User] = {
val result = userService.createUser(username, email)
result match {
case Right(user) =>
userRepo.save(user)
Right(user)
case Left(error) => Left(error)
}
}
def getUser(id: Long): Option[User] = {
userRepo.findById(id)
}
def generateReport(): String = {
val users = userRepo.list()
val timestamp = LocalDateTime.now()
val reportId = UUID.randomUUID().toString
s"""User Report
|ID: $reportId
|Generated: $timestamp
|Total Users: ${users.length}
|Active Users: ${users.count(_.isActive)}
|""".stripMargin
}
}
Selective Imports and Exclusions
package com.company.myapp.utils
// Import everything except specific items
import java.util.{List => _, Map => _, _} // Import all except List and Map
// Import with renaming to avoid conflicts
import scala.collection.{mutable => mut, immutable => imm}
// Import specific members from an object
import scala.math.{Pi, sqrt, pow}
// Import nested packages
import com.company.myapp.data.repositories.impl._
object MathUtils {
// Using renamed imports
def calculateArea(radius: Double): Double = Pi * pow(radius, 2)
def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2))
}
// Using mutable collections with alias
def createMutableSet[T](items: T*): mut.Set[T] = {
val set = mut.Set.empty[T]
items.foreach(set.add)
set
}
// Using immutable collections with alias
def createImmutableList[T](items: T*): imm.List[T] = {
imm.List(items: _*)
}
}
// File-level imports (outside of classes/objects)
import com.company.myapp.models._
import com.company.myapp.services._
object ApplicationMain {
def main(args: Array[String]): Unit = {
val userService = new UserService()
userService.createUser("alice", "alice@example.com") match {
case Right(user) => println(s"Created user: ${user.displayName}")
case Left(error) => println(s"Error: $error")
}
}
}
Import Scope and Positioning
package com.company.myapp.advanced
class DatabaseManager {
// Imports can be placed inside classes/methods for limited scope
import java.sql.{Connection, DriverManager, PreparedStatement}
import scala.util.Using
private val url = "jdbc:h2:mem:testdb"
def executeQuery(sql: String): List[Map[String, Any]] = {
// Import specific to this method
import scala.collection.mutable.ListBuffer
val results = ListBuffer.empty[Map[String, Any]]
Using(DriverManager.getConnection(url)) { conn =>
Using(conn.prepareStatement(sql)) { stmt =>
val rs = stmt.executeQuery()
while (rs.next()) {
// Method-scoped import
import scala.collection.mutable.Map as MutableMap
val row = MutableMap.empty[String, Any]
val metaData = rs.getMetaData
for (i <- 1 to metaData.getColumnCount) {
row(metaData.getColumnName(i)) = rs.getObject(i)
}
results += row.toMap
}
}
}
results.toList
}
def withTransaction[T](block: Connection => T): T = {
// Block-scoped imports
import java.sql.SQLException
Using(DriverManager.getConnection(url)) { conn =>
try {
conn.setAutoCommit(false)
val result = block(conn)
conn.commit()
result
} catch {
case _: SQLException =>
conn.rollback()
throw new RuntimeException("Transaction failed")
}
}.get
}
}
Package Objects
Creating Package Objects
// File: src/main/scala/com/company/myapp/utils/package.scala
package com.company.myapp
// Package object - provides utilities for the entire package
package object utils {
// Type aliases
type UserId = Long
type ProductId = String
type Price = BigDecimal
// Constants
val DefaultPageSize: Int = 20
val MaxPageSize: Int = 100
val ApiVersion: String = "v1"
// Utility functions available throughout com.company.myapp.utils package
def formatCurrency(amount: Price): String = f"$$${amount}%.2f"
def generateId(): String = java.util.UUID.randomUUID().toString
def isValidEmail(email: String): Boolean = {
val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$$"""
email.matches(emailRegex)
}
def paginate[T](items: List[T], page: Int, pageSize: Int = DefaultPageSize): (List[T], Int) = {
val safePageSize = math.min(pageSize, MaxPageSize)
val offset = (page - 1) * safePageSize
val pageItems = items.slice(offset, offset + safePageSize)
val totalPages = math.ceil(items.length.toDouble / safePageSize).toInt
(pageItems, totalPages)
}
// Implicit conversions
implicit class StringExtensions(s: String) {
def slugify: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-").stripPrefix("-").stripSuffix("-")
def truncate(maxLength: Int): String = if (s.length <= maxLength) s else s.take(maxLength - 3) + "..."
}
implicit class PriceExtensions(price: Price) {
def formatted: String = formatCurrency(price)
def withTax(taxRate: Double = 0.1): Price = price * (1 + taxRate)
}
}
// File: src/main/scala/com/company/myapp/models/package.scala
package com.company.myapp
package object models {
// Domain-specific type aliases
type UserEmail = String
type Username = String
// Validation functions
def validateUsername(username: Username): Either[String, Username] = {
if (username.length < 3) Left("Username must be at least 3 characters")
else if (username.length > 50) Left("Username must be at most 50 characters")
else if (!username.matches("[a-zA-Z0-9_]+")) Left("Username can only contain letters, numbers, and underscores")
else Right(username)
}
def validateEmail(email: UserEmail): Either[String, UserEmail] = {
if (email.contains("@") && email.length > 5) Right(email)
else Left("Invalid email format")
}
// Status enumeration
object UserStatus extends Enumeration {
type UserStatus = Value
val Active, Inactive, Suspended, Deleted = Value
}
}
// Usage of package objects
// File: src/main/scala/com/company/myapp/services/ProductService.scala
package com.company.myapp.services
import com.company.myapp.models._
import com.company.myapp.utils._ // Imports everything from package object
class ProductService {
private var products: Map[ProductId, Product] = Map.empty
def createProduct(name: String, price: Price, category: String): Product = {
val product = Product(
id = generateId(), // From utils package object
name = name,
price = price,
category = category
)
products = products + (product.id -> product)
product
}
def listProducts(page: Int = 1): (List[Product], Int) = {
val productList = products.values.toList
paginate(productList, page) // From utils package object
}
def formatPrice(productId: ProductId): Option[String] = {
products.get(productId).map(_.price.formatted) // Using implicit extension
}
def generateSlug(name: String): String = {
name.slugify // Using implicit string extension
}
}
Advanced Package Organization
// File: src/main/scala/com/company/myapp/config/package.scala
package com.company.myapp
package object config {
// Configuration case classes
case class DatabaseConfig(
host: String,
port: Int,
database: String,
username: String,
password: String,
maxConnections: Int = 10
)
case class ServerConfig(
host: String = "localhost",
port: Int = 8080,
ssl: Boolean = false
)
case class AppConfig(
server: ServerConfig,
database: DatabaseConfig,
environment: String = "development"
) {
def isProduction: Boolean = environment == "production"
def isDevelopment: Boolean = environment == "development"
}
// Configuration loading
object ConfigLoader {
def load(): AppConfig = {
// In real app, this would load from application.conf, environment variables, etc.
AppConfig(
server = ServerConfig(),
database = DatabaseConfig(
host = sys.env.getOrElse("DB_HOST", "localhost"),
port = sys.env.get("DB_PORT").map(_.toInt).getOrElse(5432),
database = sys.env.getOrElse("DB_NAME", "myapp"),
username = sys.env.getOrElse("DB_USER", "user"),
password = sys.env.getOrElse("DB_PASS", "password")
),
environment = sys.env.getOrElse("ENV", "development")
)
}
}
}
// File: src/main/scala/com/company/myapp/api/package.scala
package com.company.myapp
package object api {
// API response types
case class ApiResponse[T](
data: Option[T] = None,
error: Option[String] = None,
timestamp: java.time.LocalDateTime = java.time.LocalDateTime.now()
)
case class PaginatedResponse[T](
data: List[T],
page: Int,
totalPages: Int,
totalItems: Int
)
// HTTP status codes
object HttpStatus {
val OK = 200
val CREATED = 201
val BAD_REQUEST = 400
val NOT_FOUND = 404
val INTERNAL_SERVER_ERROR = 500
}
// Helper functions
def success[T](data: T): ApiResponse[T] = ApiResponse(data = Some(data))
def error(message: String): ApiResponse[Nothing] = ApiResponse(error = Some(message))
def paginated[T](items: List[T], page: Int, totalPages: Int): PaginatedResponse[T] = {
PaginatedResponse(items, page, totalPages, items.length)
}
// Request validation
implicit class RequestValidation(request: Map[String, String]) {
def getRequired(key: String): Either[String, String] = {
request.get(key).toRight(s"Missing required parameter: $key")
}
def getOptional(key: String): Option[String] = request.get(key)
def getInt(key: String): Either[String, Int] = {
request.get(key) match {
case Some(value) =>
try Right(value.toInt)
catch { case _: NumberFormatException => Left(s"Invalid integer value for $key: $value") }
case None => Left(s"Missing required parameter: $key")
}
}
}
}
Visibility and Access Modifiers
Package-Private and Protected Access
package com.company.myapp.security
// Base class with different access levels
abstract class SecurityContext {
// Public - accessible from anywhere
val sessionId: String = java.util.UUID.randomUUID().toString
// Package-private - accessible within com.company.myapp.security package
private[security] val internalToken: String = generateToken()
// Protected - accessible in subclasses
protected val secretKey: String = "secret-key-123"
// Private - only accessible within this class
private val internalState: String = "internal"
// Protected method
protected def generateToken(): String = {
s"token_${System.currentTimeMillis()}_${scala.util.Random.alphanumeric.take(10).mkString}"
}
// Package-private method
private[security] def reset(): Unit = {
println(s"Resetting security context: $sessionId")
}
// Public abstract method
def authenticate(credentials: Map[String, String]): Boolean
}
class UserSecurityContext(userId: Long) extends SecurityContext {
// Can access protected members
private val userToken = generateToken() // Using protected method
private val key = secretKey // Using protected field
def authenticate(credentials: Map[String, String]): Boolean = {
// Implementation using protected members
credentials.get("token").contains(userToken)
}
// Package-private method
private[security] def getUserId: Long = userId
}
class AdminSecurityContext extends SecurityContext {
// Admin-specific implementation
def authenticate(credentials: Map[String, String]): Boolean = {
credentials.get("role").contains("admin") &&
credentials.get("key").contains(secretKey) // Using protected field
}
// Can access package-private members
def debugInfo(): String = {
s"Session: $sessionId, Token: $internalToken" // Using package-private field
}
}
// Another class in the same package
class SecurityManager {
private var contexts: Map[String, SecurityContext] = Map.empty
def createUserContext(userId: Long): UserSecurityContext = {
val context = new UserSecurityContext(userId)
contexts = contexts + (context.sessionId -> context)
// Can access package-private members
println(s"Internal token: ${context.internalToken}") // Package-private access
context.reset() // Package-private method
context
}
def resetAll(): Unit = {
contexts.values.foreach(_.reset()) // Package-private method access
}
}
Cross-Package Access Control
// File: src/main/scala/com/company/myapp/data/Database.scala
package com.company.myapp.data
class Database {
// Private to the data package
private[data] val connectionPool: String = "connection-pool"
// Private to the myapp package (accessible from any subpackage of myapp)
private[myapp] val systemDatabase: String = "system-db"
// Private to the company package
private[company] val globalConfig: String = "global-config"
private[data] def executeInternal(query: String): String = {
s"Executing: $query on $connectionPool"
}
def execute(query: String): String = {
executeInternal(query)
}
}
// File: src/main/scala/com/company/myapp/data/repositories/BaseRepository.scala
package com.company.myapp.data.repositories
import com.company.myapp.data.Database
abstract class BaseRepository {
protected val db = new Database()
// Can access package-private members from parent package
protected def getConnectionInfo(): String = {
db.connectionPool // Accessible - private[data] from subpackage
}
// Can access myapp-level private members
protected def getSystemDatabase(): String = {
db.systemDatabase // Accessible - private[myapp]
}
}
// File: src/main/scala/com/company/myapp/services/BaseService.scala
package com.company.myapp.services
import com.company.myapp.data.Database
class BaseService {
private val db = new Database()
def getSystemInfo(): String = {
// Can access myapp-level private members
db.systemDatabase // Accessible - private[myapp]
// Cannot access data package private members
// db.connectionPool // This would not compile - private[data]
}
}
// File: src/main/scala/com/company/otherapp/Integration.scala
package com.company.otherapp
import com.company.myapp.data.Database
class Integration {
private val db = new Database()
def integrate(): String = {
// Can access company-level private members
db.globalConfig // Accessible - private[company]
// Cannot access myapp or data level private members
// db.systemDatabase // Would not compile - private[myapp]
// db.connectionPool // Would not compile - private[data]
}
}
Practical Examples
Modular Application Structure
// File: src/main/scala/com/company/ecommerce/models/package.scala
package com.company.ecommerce
package object models {
type UserId = Long
type ProductId = String
type OrderId = String
type CategoryId = String
case class Money(amount: BigDecimal, currency: String = "USD") {
def +(other: Money): Money = {
require(currency == other.currency, "Currency mismatch")
Money(amount + other.amount, currency)
}
def *(multiplier: Int): Money = Money(amount * multiplier, currency)
def formatted: String = f"$amount%.2f $currency"
}
object OrderStatus extends Enumeration {
type OrderStatus = Value
val Pending, Confirmed, Shipped, Delivered, Cancelled = Value
}
}
// File: src/main/scala/com/company/ecommerce/models/User.scala
package com.company.ecommerce.models
case class User(
id: UserId,
username: String,
email: String,
firstName: String,
lastName: String,
isActive: Boolean = true
) {
def fullName: String = s"$firstName $lastName"
}
// File: src/main/scala/com/company/ecommerce/models/Product.scala
package com.company.ecommerce.models
case class Product(
id: ProductId,
name: String,
description: String,
price: Money,
categoryId: CategoryId,
inStock: Boolean = true
)
// File: src/main/scala/com/company/ecommerce/models/Order.scala
package com.company.ecommerce.models
import OrderStatus._
case class OrderItem(productId: ProductId, quantity: Int, unitPrice: Money) {
def total: Money = unitPrice * quantity
}
case class Order(
id: OrderId,
userId: UserId,
items: List[OrderItem],
status: OrderStatus = Pending,
createdAt: java.time.LocalDateTime = java.time.LocalDateTime.now()
) {
def total: Money = items.map(_.total).reduce(_ + _)
def itemCount: Int = items.map(_.quantity).sum
}
// File: src/main/scala/com/company/ecommerce/services/package.scala
package com.company.ecommerce
package object services {
// Service exceptions
case class ServiceException(message: String, cause: Option[Throwable] = None)
extends Exception(message, cause.orNull)
case class ValidationException(errors: List[String])
extends Exception(s"Validation failed: ${errors.mkString(", ")}")
case class NotFoundException(resourceType: String, id: String)
extends Exception(s"$resourceType not found: $id")
// Result types
type ServiceResult[T] = Either[ServiceException, T]
def success[T](value: T): ServiceResult[T] = Right(value)
def failure[T](message: String): ServiceResult[T] = Left(ServiceException(message))
def notFound[T](resourceType: String, id: String): ServiceResult[T] =
Left(NotFoundException(resourceType, id))
}
// File: src/main/scala/com/company/ecommerce/services/UserService.scala
package com.company.ecommerce.services
import com.company.ecommerce.models._
class UserService {
private var users: Map[UserId, User] = Map.empty
private var nextId: UserId = 1
def createUser(username: String, email: String, firstName: String, lastName: String): ServiceResult[User] = {
if (users.values.exists(_.username == username)) {
failure("Username already exists")
} else if (users.values.exists(_.email == email)) {
failure("Email already exists")
} else {
val user = User(nextId, username, email, firstName, lastName)
users = users + (nextId -> user)
nextId += 1
success(user)
}
}
def findUser(id: UserId): ServiceResult[User] = {
users.get(id) match {
case Some(user) => success(user)
case None => notFound("User", id.toString)
}
}
def updateUser(id: UserId, updates: User => User): ServiceResult[User] = {
users.get(id) match {
case Some(user) =>
val updatedUser = updates(user)
users = users + (id -> updatedUser)
success(updatedUser)
case None => notFound("User", id.toString)
}
}
}
// File: src/main/scala/com/company/ecommerce/Application.scala
package com.company.ecommerce
// Import all models and services
import models._
import services._
// Import specific utilities
import com.company.ecommerce.models.OrderStatus._
object Application {
def main(args: Array[String]): Unit = {
val userService = new UserService()
// Create users
val user1Result = userService.createUser("alice", "alice@example.com", "Alice", "Johnson")
val user2Result = userService.createUser("bob", "bob@example.com", "Bob", "Smith")
(user1Result, user2Result) match {
case (Right(alice), Right(bob)) =>
println(s"Created users: ${alice.fullName}, ${bob.fullName}")
// Create products
val laptop = Product("P001", "Gaming Laptop", "High-performance laptop", Money(BigDecimal("1299.99")), "ELECTRONICS")
val mouse = Product("P002", "Wireless Mouse", "Ergonomic wireless mouse", Money(BigDecimal("49.99")), "ELECTRONICS")
// Create order
val orderItems = List(
OrderItem(laptop.id, 1, laptop.price),
OrderItem(mouse.id, 2, mouse.price)
)
val order = Order(
id = "ORD001",
userId = alice.id,
items = orderItems,
status = Confirmed
)
println(s"Order ${order.id}: ${order.itemCount} items, total: ${order.total.formatted}")
case (Left(error), _) => println(s"Failed to create alice: ${error.message}")
case (_, Left(error)) => println(s"Failed to create bob: ${error.message}")
}
}
}
Summary
In this lesson, you've mastered packages and imports for organizing Scala code:
✅ Package Structure: Creating logical hierarchies and nested packages
✅ Import Management: Selective imports, aliases, and scoping
✅ Package Objects: Shared utilities and type definitions
✅ Visibility Control: Package-private, protected, and cross-package access
✅ Modular Architecture: Building well-organized applications
✅ Best Practices: Code organization patterns for maintainability
Proper package organization is essential for building scalable Scala applications that are easy to maintain and navigate.
What's Next
In the next lesson, we'll explore "Visibility and Access Control: Securing Your Code." You'll learn advanced techniques for controlling access to your classes and methods, creating clean APIs, and implementing security through proper encapsulation.
This will complete your understanding of Scala's organizational features and prepare you for building production-ready applications.
Ready to master access control? Let's continue!
Comments
Be the first to comment on this lesson!