CTCaliberTek
Course: Scala 3 Developer Path: From First Program to a Tested Project · Lesson 5 of 8Log in to track your progress and earn a certificate when you finish the course.
Programming

Scala Classes, Case Classes, Objects, and Traits

Published November 9, 2026·Updated November 9, 2026·10 min read

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

Scala is fully object-oriented, but it removes much of the ceremony found in Java. A class can declare its fields in the constructor, case classes give you equality, printing, and copying for free, objects are built-in singletons, and traits let you mix shared behavior into several classes. This lesson shows how to model a small domain with these tools.

Prerequisites

  • Functions and collections from the previous lessons

Step-by-Step Guide

Step 1: Declare a class with constructor fields

The parameters in a class header become the constructor, and marking one with val makes it a public read-only field, while var makes it a public mutable field. Without either keyword the parameter is private to the class. The body can add methods and extra fields. Create instances by calling the class name, because Scala 3 allows creating an instance without the new keyword. Keep fields as vals wherever possible so an object cannot change after it is created.

class Account(val owner: String, private var balance: Double):
  def deposit(amount: Double): Unit =
    require(amount > 0, "Deposit must be positive")
    balance += amount

  def currentBalance: Double = balance

val acct = Account("Asha", 100.0)
acct.deposit(50)
println(acct.currentBalance)   // 150.0
println(acct.owner)            // Asha

Step 2: Model data with case classes

A case class is for data. By adding the case keyword, you get several useful members automatically: fields are public vals, equality compares contents rather than references, toString prints the values, hashCode is consistent with equality, and a copy method makes a modified duplicate. This replaces dozens of lines of Java boilerplate. Because case class instances are immutable, you update them by copying with changes, which makes code easier to reason about, especially in concurrent programs.

case class Employee(name: String, dept: String, salary: Double)

val e1 = Employee("Asha", "Eng", 85000)
val e2 = Employee("Asha", "Eng", 85000)
println(e1)              // Employee(Asha,Eng,85000.0)
println(e1 == e2)        // true (same contents)

val raised = e1.copy(salary = e1.salary * 1.1)
println(raised.salary)   // about 93500

Step 3: Use objects and companion objects

An object declaration creates a single instance, a singleton, which replaces static members in Scala: it is the natural home for constants and utility methods. A companion object is an object with the same name as a class in the same file, and it holds factory methods and constants related to the class. Companions can access each other's private members. A very common use is a validated factory method that returns an Option, so invalid data never becomes an instance.

case class Email(value: String)

object Email:
  def parse(text: String): Option[Email] =
    if text.contains("@") then Some(Email(text.trim.toLowerCase))
    else None

println(Email.parse("Asha@Example.com"))   // Some(Email(asha@example.com))
println(Email.parse("not-an-email"))       // None

Step 4: Share behavior with traits

A trait defines methods and fields that classes can include. It is similar to a Java interface that may also contain implementations, and a class can extend a class and mix in several traits. An abstract method has no body, and each class must supply it, while a concrete method is inherited as is. Use a trait to describe a capability, such as being printable or being auditable, and keep traits small and focused so they combine cleanly.

trait Describable:
  def name: String
  def describe: String = s"I am $name"

trait Greets:
  def greet(other: String): String = s"Hello, $other!"

class Robot(val name: String) extends Describable with Greets

val r = Robot("R2")
println(r.describe)        // I am R2
println(r.greet("Asha"))   // Hello, Asha!

Step 5: Extend a class and override members

A subclass extends a parent and can replace a method by marking it override, which the compiler requires so you cannot override by accident. Fields in the parent that are not final can be given new values through the constructor. Inheritance is useful, but Scala code leans on composition and traits more than deep class hierarchies, because long inheritance chains become hard to follow. Mark a class final to prevent extension, or use sealed to restrict subclasses to the same file, which matters for pattern matching in the next lesson.

class Shape(val name: String):
  def area: Double = 0.0
  override def toString: String = s"$name with area $area"

class Square(side: Double) extends Shape("Square"):
  override def area: Double = side * side

println(Square(3))     // Square with area 9.0

Step 6: Organize with packages and imports

A package groups related code and prevents name clashes. A file starts with a package clause, and other files bring names into scope with import, using a wildcard to bring in everything, which Scala 3 writes as an asterisk (the underscore still works). You can import several names at once from one package, and rename an import to avoid a clash. Directory layout conventionally mirrors the package name, which build tools such as sbt expect. Even for small programs, organizing code into packages from the start makes it much easier to grow.

package com.calibertek.hr

import scala.collection.mutable.ArrayBuffer
import java.time.{LocalDate, Month}
import scala.util.*

val today: LocalDate = LocalDate.of(2026, Month.NOVEMBER, 8)

FAQs

When should I use a case class instead of a regular class?
Use a case class for data whose identity is its contents, such as records, messages, and configuration. Use a regular class for objects with behavior and hidden state, such as a connection or an account with changing balance.
What is the difference between a trait and an abstract class?
A class can extend only one parent class but mix in many traits. Traits cannot take constructor parameters in Scala 2 and have a more flexible role in Scala 3. Use a trait for a capability, and an abstract class when you need a shared base with constructor arguments.
Do I need the new keyword?
Not in Scala 3. Calling Employee("Asha", "Eng", 85000) creates an instance for classes in general, so new is optional, and you will still see it in older code.

Summary

You can now define classes with constructor fields, model data with case classes, group helpers in companion objects, share behavior through traits, extend and override members, and organize code into packages. Case classes combined with sealed hierarchies are the heart of the next lesson, where pattern matching lets you take such data apart safely.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.