Scala 3 Odyssey: Part 5 — Software architecture and domain modeling

Antonel Ernest Pazargic
8 min readJun 17, 2024

--

Generated using OpenAI DALL-E

Introduction

In previous articles I’ve just scratched the surface of the scala development toolchain. But, having the right tools at our disposal is essential, as they are the pillars of the scala applications development.

In this part, after a theoretical part about software architecture and design, I am going to explain a bit the application business domain (logic and model) and then diving in the business logic expressed in scala 3.

Software architecture

More often than not I used to jump right into the implementation of the application’s requirements without thinking (too much or at all) about the application’s architecture.
This approach led to a lot of problems, such as spaghetti code, tight coupling, and overall, a difficult to maintain code.
Moreover, aspects such as modularization, scalability, multithreading/concurrency, domain modeling and testing were not properly considered in advance.

Fortunately in the recent years I’ve embraced the idea that one of the most important think to have in mind is to establish the software architecture and design right at the beginning of application lifecycle.
In the end anyone interested in the success of an application will be happy to see it being scalable, maintainable, performant, testable and extensible. And all these traits are the result of a well thought software architecture and design.

What is software architecture?

Software architecture is the blueprint of the application, and it defines the structure of the application, the components, the relationships between them, and the principles guiding their design and evolution.

There are many architecture guide lines and criteria to establish the application’s software architecture.

Many architecture styles and patterns have been proposed and established over the years, such as:

  • MVC (Model-View-Controller),
  • MVVM (Model-View-ViewModel),
  • MVP (Model-View-Presenter),
  • Event-Driven Architecture
  • SOA (Service-Oriented Architecture)
  • Microservices
  • Multi-Tier Architecture (n-Tier)

Over the years I’ve used (in production application) some of the above mentioned architectures.
But, in the recent years, I’ve started to look at the software architecture from a different perspective, and I’ve discovered some new (at least to me) architecture styles that caught my attention:

  • onion architecture,
  • hexagonal architecture

Note: There is even a a combination of them, Onionex, as Daniel Ciocirlan coin the term.

Onion architecture

The onion architecture is a layered architecture that divides the application into layers, and each layer has a specific responsibility.
It emphasizes the separation of concerns throughout of the system and it controls coupling.
The fundamental rule is that one layer can only depend on the layer beneath it, and not on the layers above it.

Onion layers:

A. The inner layers forms application core, which contains:

  • domain model,
  • business logic (data transformation),
  • application services.

B. the outer layer(s) contains:

  • infrastructure layer,
  • user interface layer (which might also be CLI commands and options)
  • test layer — used to test the application core.

Another benefit from the onion architecture is that it makes the application easy to test, as the application core is pure and side-effect free, so that the technical solutions which go into the infrastructure layer can be switched to something else without affecting the application core.

Onion Architecture

Hexagonal architecture

This is a ports and adapters architecture that divides the application into:

  • hexagon: the core business logic,
  • ports: interfaces that define integration points with the external world. Ports definitions are in the application core (hexagon).
  • adapters (implementations of the ports). They resides outside of the hexagon, in the infrastructure layer.

The ports are the interfaces that define the application’s behavior, and the adapters are the implementations of the ports.

There are two types of ports:

  • primary ports (driven ports): they are the entry points into the application, like REST API, CLI, etc.
  • secondary ports (driving ports): they are the exit points from the application, like databases, messaging systems, etc.

One of the key benefits of the hexagonal architecture is that it makes the application independent of the external world (especially technologies and frameworks), and it makes the application easy to test and maintain.

Thanks to Carlos Mayo (https://wata.es/hexagonal-architecture-introduction-and-structure/)

So that I’ve decided to chose onion architecture, and later on I might switch to hexagonal architecture if it’ll make the code more testable and maintainable, and easier to reason about.

Side note: The two architecture types I’ve presented are better suited to OOP (Object Oriented Programming), and I am not sure how they can be adapted to FP (Functional Programming).
If (when) times come for functional architecture design, I believe I’ll have to take a good look at the ideas expressed by Alexander Granin in his book “Functional Design and Architecture” (see also the references section).

Now that we have a relatively good understanding of the software architecture, and decided which one to use, let’s move on to the application core layers.

Application core

Application core is at the heart of the application, and it is compound of the business model (data representation) and business logic (data transformations).

Business model (data)

The business model is the inner most layer of the application, and it represents the data the application works with.

Project (business domain): STonic Conference

The structure of the data model

  • TicketType: Free, Paid[price]
  • SponsorshipType: Platinum, Gold, Silver
  • ConferenceType (Technical: [technology], Environmental: [aspect, zone], Medical: [specialization, disease])
  • Address (street, city, country, zipCode)
  • Contact (email, phone)
  • Organization (name, address, contact)
  • Venue (name, address, capacity)
  • Speaker (name, organization, contact)
  • Talk (title, optional[description], speakers, duration)
  • Workshop (title, optional[description], speakers, duration, capacity)
  • Attendee (name, optional[contact], ticket)
  • Sponsor (name, sponsorshipType)
  • Conference (name, organization, conferenceType, venue, contact, attendees, speakers, talks, workshops, sponsors, geoLocation)

A big picture worth a thousand words, so here it is the model diagram.

Data model

Scala 3 features helping with the definition of the business model

  • case classes (ADT product type),
  • enums (ADT sum type),
  • sealed traits (ADT sum type for scala 2.x)
  • value classes (type safe wrappers),
  • type aliases (type synonyms),
  • non-mandatory params:
    - union type,
    - option type,
  • collections,
  • tuples (collections of different types),
  • opaque types,
  • named, default parameters,
  • immutability (for instance, case classes’ copy method),
  • modules by using objects, access restriction (visibility),
  • companion object,
  • given/using (conversions)

Business model translated to Scala 3

Feeding data into the business model.

And, in the end, a simple scala application.

Before going to the next section see below the project structure.

Project structure

What is ADT?

I’ve mentioned above ADT — Algebraic Data Types — so it deserve an explanation.
ADT is a data type composed of a finite set of values.
There are two types of ADT:

  • product types: which are composed of two or more types, and they are represented by case classes in scala.
  • sum types: they represents choices between two or more types, and they are represented by enums in scala 3.

Speaking briefly of ADT triggers the need to mention what algebra is, in the context of functional programming.
Algebra defines operations on data and the rules that govern these operations.

  • operations are functions in the context of programming,
  • laws are the rules that govern these functions.

A straightforward example is the addition operation, which has the following laws:
- associativity: (a + b) + c = a + (b + c)
- commutativity: a + b = b + a
- identity: a + 0 = a
- inverse: a + (-a) = 0

Case classes

Case classes are ADT product types in scala, types that are composed of two or more types.
Their main purpose is to hold data, and their key characteristics are immutability and pattern matching.
They implements by default the following methods:

  • equals,
  • hashCode,
  • toString,
  • copy (create a new instance with some fields changed),
  • apply (create a new instance without the new keyword),
  • unapply (destructuring)

Below is an example of the Address case class, and its usage.

Enums

Enums are ADT sum types in Scala 3.
Their main purpose is to represent a fixed number of possible values.
Enums can have parameters, and they can be used in pattern matching.

A simple enum example

The enums key characteristics are:

  • immutability,
  • pattern matching,
  • exhaustiveness checking,
  • type safety

The 3rd characteristic, exhaustiveness checking, is a feature that ensures that all possible values are handled in the pattern matching, and it is a great help in avoiding bugs.
Let’s suppose that a new entry is added to the TicketType enum, like EarlyBird, and we forget to handle it in the pattern matching.
Then the compiler will raise a warning, and we will be forced to handle it, thus avoiding a potential bug.

Compilation warn when the pattern match is not exhaustive

If we are stuck to scala 2.x, or have a preference of using ADT sum types with sealed traits, here is the equivalent of the above enum.

References

  • A summary of all my articles in this mini-series
  • Onion Architecture
  • Hexagonal Architecture
  • RockTheJvm: Onionex Architecture
  • Scott Wlaschin — Software Architecture
  • RockTheJVM — ADT
  • ADT — RockTheJVM
  • Functional Design and Architecture by Alexander Granin — 1st Ed.
  • Functional Design and Architecture by Alexander Granin — 2nd Ed.

Closing words

In this part I’ve gone through software architecture, then I’ve presented the business model (data) of the application (STonic Conference), and I’ve continued by explaining the Scala 3 features that help with the definition of the Business Model.
In the end I’ve dive a bit into two of the main scala features when it comes to defining the business model: case classes and enums.

The STonic Conference project’s source code can be found at jtonic/stonic-conference (github.com)
You can run the application and tests with our new friends: sbt run & sbt test .

In the next articles I am going to talk about the other scala syntactic constructs and language features that I used in the business model.
Then I aim to explore the business logic (data transformations) of the application, and what scala 3 has under its sleeve to help with that.

I am looking forward to the questions, suggestions, and feedback you might have.
If you’ve liked the article please share it with whomever you think might benefit from it, and don’t forget to clap.

Thank you for reading!

--

--