Abstract Classes in Scala
Imagine you're building an e-commerce application that supports multiple payment methods:
Credit Card
UPI
Net Banking
Wallet
Every payment method must support a common operation called processPayment().
However, the implementation will be different for each payment method.
A credit card payment may communicate with a card gateway, while a UPI payment may use a UPI provider.
How can we define a common structure without implementing the same payment logic for every payment method?
Scala provides abstract classes to solve this problem.
What Is an Abstract Class?
An abstract class is a class that cannot be instantiated directly.
It is designed to be extended by other classes.
An abstract class can contain:
Abstract methods without implementations.
Concrete methods with implementations.
Fields and properties.
Constructors.
Business logic shared by subclasses.
Let's look at a simple example.
abstract class Employee {
val name: String
def work(): Unit
def company(): Unit = {
println("Working at CS Techie")
}
}Here:
Employeeis an abstract class.nameis an abstract field.work()is an abstract method.company()is a concrete method.
An abstract method defines what a subclass must implement, while a concrete method provides reusable behavior.
Implementing an Abstract Class
A subclass extends an abstract class and provides implementations for its abstract members.
abstract class Employee {
val name: String
def work(): Unit
def company(): Unit = {
println("Working at CS Techie")
}
}
class Developer extends Employee {
override val name: String = "Rahul"
override def work(): Unit = {
println(s"$name is writing code")
}
}
object Main extends App {
val developer = new Developer()
developer.company()
developer.work()}Output:
Working at CS Techie
Rahul is writing code
Notice that Developer provides implementations for both abstract members.
The company() method is already implemented in the parent class, so the child class can reuse it directly.
Abstract Methods vs Concrete Methods
An abstract class can contain both types of methods.
Let's understand the difference.
| Abstract Method | Concrete Method |
|---|---|
| Has no implementation | Has an implementation |
| Must be implemented by a concrete subclass | Can be inherited and reused |
| Defines a required behavior | Provides shared behavior |
| Declared without a method body | Declared with a method body |
Example:
abstract class Payment {
def processPayment(amount: Double): Unit
def paymentStatus(): Unit = {
println("Payment processing initiated")
}
}Here, processPayment() is abstract, while paymentStatus() is concrete.
A subclass must implement processPayment() before it can be instantiated, but it can reuse paymentStatus().
Abstract Classes with Constructors
An abstract class can have a constructor just like a normal class.
This is useful when all subclasses need common initialization data.
abstract class Employee(
val id: Int, val name: String) {
def role(): String
def displayDetails(): Unit = {
println(s"Employee ID: $id")
println(s"Name: $name")
println(s"Role: ${role()}")
}
}
class Developer( id: Int, name: String, val technology: String) extends Employee(id, name) {
override def role(): String = {
"Software Developer"
}
}
object Main extends App {
val developer = new Developer(101, "Rahul", "Scala") d
eveloper.displayDetails()
println(s"Technology: ${developer.technology}")
}Output:
Employee ID: 101
Name: Rahul
Role: Software Developer
Technology: Scala
The Employee constructor initializes the common employee properties.
The Developer class passes its constructor arguments to the parent using:
extends Employee(id, name)
This avoids repeating common initialization logic in every subclass.
Abstract Classes and Runtime Polymorphism
Abstract classes work particularly well with polymorphism.
You can create a reference using the abstract parent type and assign different child objects to it.
abstract class Payment {
def processPayment(amount: Double): Unit}class CreditCardPayment extends Payment {
override def processPayment(amount: Double): Unit = {
println(s"Credit Card payment: $amount")
}
}
class UpiPayment extends Payment {
override def processPayment(amount: Double): Unit = {
println(s"UPI payment: $amount")
}
}
class NetBankingPayment extends Payment {
override def processPayment(amount: Double): Unit = {
println(s"Net Banking payment: $amount")
}
}
object Main extends App {
val payments: List[Payment] = List(
new CreditCardPayment(),
new UpiPayment(),
new NetBankingPayment()
)
payments.foreach(_.processPayment(1000))
}Output:
Credit Card payment: 1000.0
UPI payment: 1000.0
Net Banking payment: 1000.0
The collection uses the common Payment type, but each object executes its own implementation.
This is runtime polymorphism using an abstract class.
Abstract Class vs Normal Class
Let's compare the two.
| Feature | Normal Class | Abstract Class |
|---|---|---|
| Can create objects directly | Yes | No |
| Can contain concrete methods | Yes | Yes |
| Can contain abstract methods | No | Yes |
| Can have constructors | Yes | Yes |
| Can contain fields | Yes | Yes |
| Can be extended | Yes | Yes |
Use a normal class when you have a complete implementation that can be instantiated directly.
Use an abstract class when you want to define a common foundation for related classes, with some behavior left for subclasses to implement.
Abstract Class vs Trait in Scala
Scala also provides traits, which are another way to define reusable behavior and contracts.
Both abstract classes and traits can contain abstract and concrete members.
However, there are important differences.
| Feature | Abstract Class | Trait |
|---|---|---|
| Constructor parameters | Supported | Supported in Scala 3; Scala 2.13 has no trait constructor parameters |
| Multiple inheritance | A class can extend only one class | A class can mix in multiple traits |
| Primary purpose | Common base class and shared state | Reusable behavior and contracts |
| Instantiation | Cannot instantiate directly | Cannot instantiate directly |
| Fields and methods | Supported | Supported |
For example, an employee may inherit from an abstract Employee class and mix in multiple traits such as Loggable and Auditable.
We'll explore traits in detail in the next chapter.
8. Real-World Example: Payment Processing
Let's design a small payment processing model.
Every payment must implement processPayment(), but all payment types can share a common transaction logging method.
abstract class Payment(
val transactionId: String) {
def processPayment(amount: Double): Unit def logTransaction(): Unit = {
println(s"Transaction ID: $transactionId")
}
}
class CreditCardPayment(id: String) extends Payment(id) {
override def processPayment(amount: Double): Unit = {
println(s"Processing Credit Card payment of $amount")
}
}
class UpiPayment(id: String) extends Payment(id) {
override def processPayment(amount: Double): Unit = {
println(s"Processing UPI payment of $amount")
}
}
object Main extends App {
val payment = new UpiPayment("TXN1001")
payment.logTransaction()
payment.processPayment(2500)
}Output:
Transaction ID: TXN1001
Processing UPI payment of 2500.0
The abstract class provides shared transaction information and logging behavior.
Each payment implementation provides its own processing logic.
This is a useful design when several related classes share common state and behavior but must implement certain operations differently.
In a production application, payment processing would also require gateway integration, transaction validation, idempotency, and failure handling.
9. When Should You Use Abstract Classes?
Consider an abstract class when:
Multiple related classes share common properties and behavior.
You want to enforce implementation of specific methods.
Subclasses need common constructor initialization.
You want to provide some reusable functionality while leaving other behavior customizable.
You are modeling a genuine parent-child relationship.
Avoid creating abstract classes just to group unrelated methods. A trait or standalone service may be a better fit depending on the design.
Remember: An abstract class defines the common foundation, while its subclasses provide the specialized implementation.
