Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
214: Building a Multi-Tenant SaaS Backend in Scala
Alright, let's dive into this. We're building "TaskFlow," a project management tool. The requirement is simple on paper: multi-tenancy. We want a single deployment of our Scala backend serving hundreds of different companies, but Company A must never see Company B's tasks. It sounds easy—just add a tenant_id column to every table—but as we're about to see, the "plumbing" of that is where things usually fall apart.
The "Manual Pass" Fatigue
My first instinct, and probably yours too, is to just pass the tenant ID everywhere. Let's look at how I started the TaskService. I wrote a method to fetch all tasks for a project, and it looked something like this:
case class Task(id: UUID, tenantId: UUID, title: String, projectId: UUID)
class TaskService(repo: TaskRepository) {
def getTasksForProject(tenantId: UUID, projectId: UUID): List[Task] = {
repo.findByProject(tenantId, projectId)
}
}
It works. Great. But then I added ProjectService, UserService, BillingService... and suddenly, every single method signature in my entire application starts with tenantId: UUID. It's noisy, it's redundant, and honestly, it's a cognitive burden. I'm spending more time passing the ID around than actually writing business logic. There has to be a way to make the tenant context "ambient" without losing type safety.
Turning the ID into a Context
Since we're in Scala, I'm thinking about how to hide this boilerplate. I could use a ThreadLocal, but that's a nightmare for testing and doesn't play well with asynchronous code (which we're using). Instead, let's try using a context parameter. In Scala 3, we have using clauses, which are perfect for this.
I'll define a TenantContext case class and change my service to require it implicitly:
case class TenantContext(tenantId: UUID)
class TaskService(repo: TaskRepository) {
def getTasksForProject(projectId: UUID)(using ctx: TenantContext): List[Task] = {
repo.findByProject(ctx.tenantId, projectId)
}
}
Now my business logic looks cleaner. When I call getTasksForProject(projId), the compiler just finds the TenantContext in scope. I feel like I've won. But here is where I hit a wall: I just wrote a new method to archive a task, and in my haste, I forgot to pass the tenantId to the repository's update call. I accidentally wrote a query that updated a task based only on its taskId.
This is the "SaaS Nightmare." One missing where` tenant_id = ? and Company A can archive Company B's data just by guessing a UUID. Relying on the developer to remember to use the context in every single repo call is a recipe for a security breach.
Enforcing Isolation at the Boundary
I need a guardrail. I can't trust myself (or anyone else on the team) to manually filter by tenant every time. The solution is to move the tenant filtering out of the service layer and bake it into the repository layer itself.
I'll create a base TenantRepository trait that handles the filtering logic. Instead of letting the repository methods take a tenantId as a raw argument, I'll force them to operate within a context that provides it.
trait TenantRepository[T] {
// We define a protected method that the actual DB implementation uses
protected def applyTenantFilter(query: Query[T])(using ctx: TenantContext): Query[T] = {
query.where('tenant_id'.eq(ctx.tenantId))
}
}
class TaskRepository extends TenantRepository[Task] {
def findByProject(projectId: UUID)(using ctx: TenantContext): List[Task] = {
val baseQuery = Query.for[Task].where('project_id'.eq(projectId))
// I now FORCE the filter to be applied here
db.run(applyTenantFilter(baseQuery))
}
}
By wrapping the query execution in applyTenantFilter, I've created a bottleneck. If I try to write a repository method and I don't use that helper, the code will either not compile (because it needs the TenantContext) or it will be glaringly obvious during a code review that I'm bypassing the tenant filter. It's not 100% foolproof—I could still write a raw SQL string—but it moves the risk from "every single method" to "only when I bypass the framework."
Connecting the Dots at the API Level
The last piece of the puzzle: where does the TenantContext actually come from? I don't want to manually instantiate it in every Controller action. I'll set up a Middleware (or a Filter in Play/Http4s) that extracts the X-Tenant-ID header from the incoming request and puts it into the scope.
In a real-world ZIO or Cats Effect app, I'd use a Reader monad or a ZLayer to provide this. For our purposes, the flow looks like this:
Request arrives → Middleware extracts TenantID → Middleware creates TenantContext → Context is passed into the Service → Service passes it to the Repository → Repository applies the SQL filter.
It's a bit more setup upfront, but now I can sleep at night knowing that a junior dev adding a "Delete Project" feature won't accidentally wipe out the entire database because they forgot a where clause.
📋 Practical Task
Exercise: Implementing a Tenant-Aware Audit Log
You are tasked with adding an AuditLogService to TaskFlow. This service records every major action a user takes (e.g., "Created Project", "Deleted Task"). To maintain strict isolation, the audit logs must also be tenant-isolated.
Requirements:
- Create a
AuditLogcase class containingid: UUID,tenantId: UUID,action: String, andtimestamp: LocalDateTime. - Implement an
AuditLogRepositorythat extends theTenantRepositorypattern discussed in the lesson. - Implement a
logAction(action: String)(using ctx: TenantContext)method in the repository. - Ensure that the
logActionmethod uses theapplyTenantFilterlogic (or an equivalent mechanism) to ensure the log is associated with the correct tenant from the context, rather than taking atenantIdas a direct parameter. - Write a small test case or main method that attempts to log an action for "Tenant A" and verify that the resulting record contains the correct
tenantIdfrom theTenantContext.
There are no comments for now.