Skip to content

Architectural patterns that I’m using on my Workout Scheduler

_

Table of contents

  1. Introduction
  2. Monorepository with Turbo
  3. DDD and Bounded Contexts
  4. Hexagonal Architecture
  5. Bounded Contexts Design
  6. Testing
  7. Next Steps

Introduction

For approximately one year, I have been working on my own SaaS to help me schedule my calisthenics workouts and track my progress over time (if I manage to finish it someday).

The thing is, this is the third time I'm rebuilding the project because there's always something awesome I want to learn and apply to this side project. I really hope this is the last time I rebuild it, which is why I want to share my "definitive" approach to building a large-scale application.

In this post, I want to give a brief explanation of the general topics I'm following because I think they are really good ways to build a project and can inspire those reading this to apply them to their projects.

So, with no further ado, let's go!

Monorepository with Turbo

At the highest level, the project is organized in a monorepository powered by Turborepo. The backend and frontend are two different applications, and I don't discard the idea of having other applications related to the project, like a back office to manage some internal stuff, a log viewer, or maybe splitting the backend into microservices.

The monorepo is divided into three main folders at the moment:

  • apps/ (The applications or entry points to the system)
  • packages/ (Libraries with shared code among the apps and contexts)
  • contexts/ (Business logic of the application, which I will explain in depth in the DDD section)

DDD and Bounded Contexts

When talking about software architecture and how to organize your code base, these two topics have great synergy.

DDD involves writing your code based on your business language. Literally, DDD encourages developers to translate what business people say into code, making communication easier for both. This is achieved by organizing our code by bounded contexts, where all the business logic lives.

In my application, you will hear a lot about "exercises," "workouts," "users," and "progressions," which are the bounded contexts holding the logic related to these topics.

To achieve this, DDD provides some building blocks, like value objects, aggregates, domain events, and more.

In my case, all these bounded contexts reside in the contexts/ folder, grouped into a big context called manager because this is what my application does at the moment. If I need to have some logic related to measuring metrics or statistics in the future, I'll probably create a context called statistics.

Hexagonal Architecture

I really love functional programming and have always thought it is easy to mess things up when writing code following object-oriented programming (and I still think it a little).

But if you manage to understand SOLID principles and plan to apply them consistently, you can build a consistent and scalable product.

I prefer the name "Ports and Adapters" instead of "Hexagonal Architecture" because it is more self-explanatory, but the ideas are the important thing here.

Hexagonal architecture is all about decoupling—decoupling from the database, from external systems, even from the framework you are using to build your app.

That's the main reason why I have a separate folder for the applications and the business logic. I'm only applying all of this to the backend because I find it tedious to apply to the frontend (mainly because it is quite harder to decouple from the framework in this case).

This way, you can find three different applications (or entry points) inside my apps/ folder:

  • api/ (Express API REST)
  • api-e2e/ (API E2E testing project)
  • client/ (React application)

The important application here is the REST API. In this project, you will only find:

  • Controllers
  • Routes
  • Authorization
  • Dependency injections
  • Global configuration

None of these things have anything to do with the business logic, so they are differentiated from the rest of the system.

Bounded Contexts Design

The interesting part resides in the bounded contexts. They are organized following screaming architecture, so inside each context, you will find three folders:

  • application/ (Use cases of the system)
  • domain/ (Aggregates, "contracts" or interfaces, and domain services)
  • infrastructure/ (Code that interacts with an external system or encapsulates an external tool)

Currently, there are only three contexts: users, exercises, and shared. The users and exercises contexts are self-explanatory, but in the shared context, you can find all the code used by other contexts. For example, here you can find a value object representing UUIDs.

Testing

I'm not diving in-depth into this topic because my time is limited by my full-time job and other activities I practice in my free time. However, I'm trying to implement tests following this approach:

  • Use cases must have basic unit tests
  • Infrastructure code, like repositories, has integration tests
  • The final API is tested with E2E tests

Next Steps

Currently, the application doesn't need to implement CQRS or migrate to a microservices architecture, but I would like to put these topics into practice. So, it's just a question of time to reinvent the wheel once more.

I really hope I can share my experience on how to do it in a couple of months!