In this post I will take you through the stack of a real live app I have created, outlining the technology and structural choices I have made.
The reason for writing this is to give myself and others an overview of what it has involved to go from a blank canvas to a complete (minimum viable) SaaS product.
The application in question is OnCall, a small SaaS product I have created in my spare time and launched around a month ago. The app is basic, with the following key areas of functionality.
- Team management
- Scheduling of on-call shifts
- Notifications and reminders
- Personal schedules and dashboards
- Account-related stuff (sign in, forgotten password)
- Subscriptions and payment
Let's start by taking a look the application from a high level to get an idea of the parts.
Basically the application consists of a public facing website, the application itself, a background worker and some integrations. All of the components rely on the same core library, which I will explain in more detail below.
The public website
This is the website you see at https://www.oncallschedule.com, it's just a simple website with a signup form. The tools and techniques used are the same as the application, so I won't delve into the details here, but just note that the website is run as an Azure Website, whereas the application is run as a web role.
The background worker
Some of the stuff that goes on inside OnCall isn't suitable for being handled by a web application. So things like calculating when to send notifications and reminders, as well as actually sending them is handled by a worker role.
When a user changes notification rules or a schedule, a message is queued to the background worker using Azure ServiceBus. The worker will then calculate whether there are any changes to the upcoming notification schedule for the account.
Even though these tasks are handled separately the logic exists in the same domain model in the core library that is used by the application.
It wasn't until recently I discovered exactly how powerful Azure WebJobs are. The tasks performed by the background worker could easily be done by WebJobs. I will be looking into making the transition soon as it will bring down both complexity and cost.
The application is where most of the fun stuff happens. It's what you see when visit https://mysubdomain.oncallschedule.com. The basic structure looks something like this.
- The user interacts with an AngularJS application
- The application is served by an MVC site that has no job except authentication and serving pages
- All actual interaction with the application happens through the API
- When data is required the API runs read-only queries
- The API invokes changes by sending commands
- Commands are handled by command handlers which have access to the domain models
- All business logic reside in the domain models
- The domain model might invoke events upon change
- Event handlers act upon these events to perform tasks like sending e-mails etc.
Technology and frameworks used
|AngularJS||https://angularjs.org/||The frontend is a set of AngularJS applications.|
|Bootstrap||http://getbootstrap.com/||The layout is created using the Bootstrap library|
|MomentJS||http://momentjs.com||Used for handling date and time operations|
|jQuery||http://www.jquery.com||Required for some UI plugins I use|
|MVC||https://www.asp.net||Pages are served by a basic MVC 5 application|
|WebAPI||http://www.asp.net||Interaction with the server is through a WebAPI 2 API|
|ASP.NET Identity||http://www.asp.net/identity||Handling user authentication|
|Entity Framework||https://github.com/aspnet/EntityFramework||EF6 is used for persisting the state of the domain model|
|Dapper||https://code.google.com/p/dapper-dot-net/||Dapper is used for querying for data|
|FluentMigrator||https://github.com/schambers/fluentmigrator/||Used for database migrations|
|Postmark||https://www.postmarkapp.com||For reliable e-mail dispatching|
|Twilio||https://www.twilio.com||For text messages|
|Stripe||https://www.stripe.com||For handling subscriptions and payments|
|Sentry||https://www.getsentry.com||For error logging|
|Azure Websites||https://www.windowsazure.com||For running the public facing website|
|Azure Web Roles||-||For running the multi-tenant web app|
|Azure Worker Roles||-||For doing background processing|
|Azure SQL||-||For storing all application data|
|Azure WebJobs||-||For checking that things are alive and healthy|
|Azure ServiceBus||-||For communication between roles|
|XUnit||https://xunit.codeplex.com/||For unit tests|
|FakeItEasy||https://github.com/FakeItEasy/FakeItEasy||For faking and mocking|
|FluentAssertions||http://www.fluentassertions.com/||For readable assertions in tests|
|GitHub||https://www.github.com/||For code repository|
|TeamCity||https://www.jetbrains.com/teamcity/||For continuous integration|
|Octopus Deploy||https://octopusdeploy.com/||For automatic deployment|
Thoughts about the stack
The first thing that comes to mind when I see the list above is that it requires a broad set of tools and techniques to create something, even if it is only a simple application.
The stack isn't exactly bleeding edge, the tools I have used are tried and tested. It has allowed me to move quicker during development, and to create something I can trust. It might have limited my opportunities to learn about exciting technology while working with the project, but that wasn't the purpose either, and some of learning efforts have gone into other areas like DDD and CQRS.
When it comes to ORMs I always see myself returning to Dapper. Yes, it does require hand coding your queries, but that also gives you a degree of control. I am confident I have spent much more time tracking down Entity Framework oddities than I have on keeping my Dapper SQL queries up to date.
I have used the TeamCity and Octopus combination since day one. I feel the reward for spending a few hours getting continuous delivery up and running is well worth it, even for smaller projects.
What happens next
I have a few must-dos on my list that I need to get out of the way. First of all, I have some trouble using my continuous delivery setup right now (related to updating a server in Europe while on a mobile broadband connection in the Philippines), I need to sort this out. I also have some work to do on the scheduling functionality which really isn't as good as I want it to be. I would also like to trim the frontend stuff a bit, it is slightly heavy as it is now.
With that out of the way I think it's time to start adding a proper external API, look into creating a mobile application and expanding with a couple of new features.