[In this multi-part series, I'll transform a new application into a multi-tenant experience running in the Heroku ecosystem. This article focuses on the object model, design, architecture, and security.]
My wife, Nicole has an identical twin named Danyel. While Nicole and Danyel have always maintained their individuality, they both share a passion for being in good physical shape and sticking to a workout routine. In fact, Danyel decided to build upon her fitness passion and start training others out of her personal gym located in the Las Vegas area. While hearing about the tasks Danyel worked through getting her business started (i.e. permits, legal documents, and insurance), I asked about her plan to keep track of her business.
Long story short, Danyel had been focused on getting her business running, but wasn't focused on having the necessary technology in place to meet the needs of her business. Since I am fresh off a successful conversion of my mother-in-law's application from Amazon Web Services (AWS) to Heroku (see Heroku - My New Home), I thought it was time to pitch in and help Danyel's business from an application perspective.
After a couple video calls and a quick visit to the Las Vegas area, we had determined that the new fitness application would provide the following features in the initial release:
- Client management (to manage her customers)
- Workout management (to create routines for her customers to perform)
- Session management (to create workout sessions which consist of a workout and for at least one customer)
Looking ahead, I later mapped out this chart of future functionality:
While showing the progress of the application to Nicole, she casually asked, "I wonder how many other personal trainers might be interested in having an application like the one you are building for my sister?"
Nicole's question sparked me to alter my design and create a system that could be used by more than just her sister. Nicole's comment transformed my design from a simple application that would run in Heroku to a multi-tenant application that could be utilized by multiple fitness trainers, regardless of location. From that point, I started thinking of the application in terms of a Salesforce-like experience for personal trainers.
From a technical perspective, I wanted to see just how quickly and easily I could build a SaaS solution to be used by more than one fitness trainer. From a business perspective, this solution would allow fitness instructors to not only manage their clients, workouts, and sessions, but become a single point of reference for all aspects of their business—all for a low monthly rate.
With the scaling options available in the Heroku ecosystem, the application and database could scale as needed. The income from the subscribers would fund the additional Heroku costs, keeping my personal investment costs low. More importantly, the ease-of-use with Heroku will allow me to continue building the features noted in the feature chart above and not get bogged down with infrastructure knowledge and decisions.
How do I plan to market this solution? Not really sure ... too excited to start putting my ideas in place.
However, with this new design concept, I needed to figure out how to do three things:
- design a multi-tenant application that acts like a single application
- understand how security would work and be different than a single application
- secure and protect the data, even when a crafty developer attempts to access the API
Designing a Multi-Tenant Application
According to Wikipedia, Multitenancy "refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants." In this case, I am in the process of creating a single instance of software that will serve multiple fitness trainers. As a result, the cost for each fitness trainer to use the system will be far less than if these same individuals were creating and hosting their own solution.
With the lessons learned from a year-long journey using AWS and the conversion effort from AWS to Heroku, my research was leading me to utilize Heroku for this application. I knew that multi-tenancy is not a challenge for Heroku, and their developer-focused approach was perfect for my situation. After all, I still work a full-time job and have a toddler in the home.
Given my experience with Java/Spring Boot and Angular, I decided to stick with technologies I know and respect. From a database perspective, MySQL seems to be a good fit for this application, too, which will employ the ClearDB option when running in Heroku.
In order to employ a multi-tenant design, the data (clients, workouts, sessions, etc.) for each tenant (fitness trainer) needs to be protected from other tenants. At a high level, a Tenant object was created and include a Tenant Properties object, which would provide attributes about the tenant and link to a Features object (where certain functionality could be enabled/disabled).
From an entity perspective, all of the objects created for the application would include a tenantId reference to the Tenant object. Since I am using Spring Boot as the RESTful API, below is an example of what the Client object looks like:
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "clients")
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@OneToOne
private Tenant tenant;
private boolean active;
@OneToOne
private Person person;
@OneToOne
private Address address;
}
Using this same pattern, the remaining entities were created for the initial release:
Knowing the Spring JPA has the ability to create the necessary SQL for the database layer, I decided to give this option a try. Even though I am using application.yml for my properties file, I went ahead and dropped the following application.properties file in my resources folder:
spring.jpa.properties.javax.persistence.schema-generation.scripts.action=create
spring.jpa.properties.javax.persistence.schema-generation.scripts.create-target=fitness.sql
spring.jpa.properties.javax.persistence.schema-generation.scripts.create-source=metadata
Next, I started the Spring Boot service and was able to see a fully populated Data Definition Language (DDL) script for the fitness database in a file called fitness.sql.
Using Docker, Docker Compose, and the MySQL base image, I was quickly able to get the database up and running within a few minutes. As noted above, I plan to use ClearDB in Heroku, but I am not quite ready for that step. For those not wanting to utilize Docker or even run the database locally, it is possible to create a database instance in Heroku, then use an .env file to attach to the remote instance.
Thereafter, I deleted the application.properties file and shut my RESTful API back down, for now.
Selecting a Security Provider
I have been quite impressed with the service offerings provided by Okta for the last five years. Across multiple projects, I have yet to find a scenario where the Okta toolset fails to meet the needs of the project. However, in this case, I had an end-state of Heroku for my multi-tenant application. So, I wanted to utilize the security partner they recommended.
To my surprise, Heroku actually has partnered with Okta for application security. In fact, adding Okta to your Heroku application is as simple as the following CLI command:
heroku addons:create okta
While this functionality is in Beta, it does automatically create all the necessary items required to allow your Heroku application to integrate with Okta. Click here for more information.
Security Interceptor
To protect one tenant from another, I wanted to introduce a SecurityInterceptor to enforce security for each request. This way, if a crafty user attempted to change an object key during the HTTP request, the interceptor would throw an error. As a result, all non-anonymous requests pass through the following class:
public class SecurityInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getName() != null) {
try {
// Perform the necessary logic
processRequest(SecurityContextHolder.getContext().getAuthentication().getName());
return true;
} catch (FitnessException e) {
log.error("Error code={}, message={}", e.getCode(), e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
log.error("Could not determine requester information");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
The processRequest() method will pull the necessary information out of the authentication.name attribute (from the authenticated Okta user) and attempt to retrieve the Tenant record for the request.
If the request is not successful, a FitnessException is thrown, leading to a 401 (Unauthorized) error back to the requester. However, if the request is successful, the interceptor code will create/place the following object on the request for future use:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserData {
private Tenant tenant;
private Person person;
}
Now, within the request lifecycle, references to the Tenant/Person (which is the user of the application) will be used in all HTTP requests instead of the values being provided with the request.
Looking Ahead
To summarize, at this point the following design has been put into place:
- Initial design has been finalized
- Tentative release schedule has been identified
- Multi-tenant design has been validated
- Security provided has been chosen and identified
In the next article, I am going to focus on getting the multi-tenant client and servers up and running for the 1.0.0 release, which will include the following architecture and design:
- Angular 9.1.11 (browser client)
- Spring Boot 2.3.1 (RESTful API)
- ClearDB/MySQL (Database)
- Okta (security)
- GitLab (source control and CI/CD)
- Heroku (application hosting)
Have a really great day!
Top comments (0)