Encapsulation and Access Modifiers in Scala
In the previous chapter, we learned how to create classes, objects, and constructors.
Now imagine you're building an Employee Management System. Every employee has a name, department, and salary.
Should every part of your application be allowed to change an employee's salary directly?
What if someone accidentally assigns a negative salary? Or changes it without following the company's rules?
A well-designed class should protect its data and control how that data is modified.
This is where encapsulation comes in.
1. What Is Encapsulation?
Encapsulation means keeping data and the methods that operate on that data together inside a class, while controlling how other parts of the application access them.
In simple terms, a class should manage its own data instead of allowing external code to change everything freely.
Consider this example:
class Employee(var name: String, var salary: Double)
object Main extends App {
val employee = new Employee("Rahul", 50000)
employee.salary = -10000
println(employee.salary)
}Output:
-10000.0
The code compiles, but the result is clearly incorrect. An employee now has a negative salary.
The problem is that salary is publicly mutable. Any code with access to the employee object can change it without validation.
Let's improve this design.
2. Using private to Protect Data
Scala provides the private keyword to restrict access to class members.
class Employee( val name: String, private var salary: Double) {
def getSalary: Double = salary
def increaseSalary(amount: Double): Unit =
{
if (amount > 0) {
salary += amount
}
}
}
object Main extends App {
val employee = new Employee("Rahul", 50000)
employee.increaseSalary(5000)
println(employee.getSalary)
}Output:
55000.0
Notice what changed:
salaryis private, so external code cannot access it directly.getSalaryprovides controlled read access.increaseSalary()controls how the salary changes.
If you try to execute employee.salary = -10000 outside the class, Scala reports a compilation error.
The important idea: Keep mutable data private when changes need validation or business rules.
3. Access Modifiers in Scala
Access modifiers define where a class member can be accessed.
Scala commonly uses three access levels:
| Modifier | Accessibility |
|---|---|
| Public (default) | Accessible wherever the enclosing class or member is visible |
private | Restricted to the enclosing class or scope |
protected | Accessible within the enclosing class and its subclasses |
Unlike Java, Scala does not require you to write public. Members are public by default.
Public members
class Employee(val name: String) {
def displayName(): Unit = {
println(name)
}
}
object Main extends App {
val employee = new Employee("Rahul")
employee.displayName()
println(employee.name)
}Both name and displayName()
are accessible from outside the class.
Private members
class BankAccount(private var balance: Double) {
def getBalance: Double = balance
}The balance variable is available inside the class, but external code cannot access it directly.
Protected members
A protected member can be accessed inside its class and by subclasses.
class Employee {
protected val company = "CS Techie"
}
class Developer extends Employee {
def showCompany(): Unit = {
println(company)
}
}
object Main extends App {
val developer = new Developer()
developer.showCompany()
}Output:
CS Techie
The Developer class inherits from Employee, so it can access the protected company member.
One important difference from Java: Scala's protected does not automatically provide Java-style package-level access. Scala has qualified access modifiers for that purpose.
4. Getters and Setters in Scala
In Java, you might create methods such as getSalary() and setSalary() to read and update private fields.
Scala supports the same approach, but it also provides a convenient property-like syntax.
Consider this example:
class Employee(private var currentSalary: Double) {
def salary: Double = currentSalary
def salary_=(newSalary: Double): Unit = {
if (newSalary >= 0) {
currentSalary = newSalary
}
else {
println("Salary cannot be negative")
}
}}
object Main extends App {
val employee = new Employee(50000)
employee.salary = 60000
println(employee.salary)
employee.salary = -10000
println(employee.salary)
}Output:
60000.0
Salary cannot be negative
60000.0
Here is what happens:
def salarydefines a getter.def salary_=defines a setter.employee.salary = 60000invokes the setter method.employee.salaryinvokes the getter method.
This lets you use familiar property syntax while keeping validation logic inside the class.
5. Qualified Access with private[packageName]
Sometimes you want a member to be accessible within a package but hidden from unrelated parts of the application.
Scala supports qualified access modifiers for this.
package payroll
class Employee {
private[payroll] var salary: Double = 50000
}Here, salary is accessible within the payroll package and its nested packages, but not from unrelated packages.
This is useful when multiple classes in the same module need access to internal implementation details.
You can also use other enclosing scopes, such as private[company], depending on your package structure.
6. Encapsulation vs Data Hiding
These terms are related, but they are not exactly the same.
Encapsulation means grouping data and the operations that work on that data into a single unit, such as a class.
Data hiding means restricting direct access to internal implementation details.
For example, a BankAccount class encapsulates the account balance and withdrawal logic. Declaring the balance as private provides data hiding.
In practical application design, these concepts are usually used together.
7. Real-World Example: Bank Account
Let's put everything together by building a small banking example.
class BankAccount( val accountNumber: String, private var balance: Double) {
def getBalance: Double = balance
def deposit(amount: Double): Unit = {
if (amount > 0) {
balance += amount
println(s"Deposited: $amount")
}
else {
println("Deposit amount must be positive")
}
}
def withdraw(amount: Double): Unit = {
if (amount <= 0) {
println("Withdrawal amount must be positive")
}
else if (amount > balance) {
println("Insufficient balance")
}
else {
balance -= amount
println(s"Withdrawn: $amount")
}
}
}
object Main extends App {
val account = new BankAccount("ACC101", 10000)
account.deposit(5000)
account.withdraw(3000)
account.withdraw(20000)
println(s"Final Balance: ${account.getBalance}")
}Output:
Deposited: 5000.0
Withdrawn: 3000.0
Insufficient balance
Final Balance: 12000.0
Notice how the account manages its own balance.
External code cannot directly assign an arbitrary value to balance. It must use the operations provided by the class.
This is the real benefit of encapsulation: business rules stay close to the data they protect.
In a production banking application, you would also need transaction management, concurrency control, authorization, and a decimal-safe monetary type such as BigDecimal.
8. When Should You Use Encapsulation?
Use encapsulation when:
Data must follow business rules before it can be changed.
You want to prevent invalid object states.
Several operations need to work with the same internal data.
You want to change the internal implementation without affecting callers.
You need to expose only the operations that other classes actually require.
A useful design habit is to prefer meaningful operations such as deposit() and increaseSalary() over unrestricted setters.
