Monolith to Service
There is a classic pattern while building successful products. Initially, it’s a relatively tightly focused, designed to fit a specific and well-defined need or interconnected set of needs. When it’s released, it’s great, gets widely used and everyone’s happy. Part of its success is its tight focus. But success breeds opportunity, so new tools, features, and functionality get added. In the classic pattern, these additions extend the same codebase and the application grows.
Each addition to the codebase, each new feature, adds to the complexity of the system and increases its fragility. A simple addition — to patch a security issue, for example — can cause a major headache for fear of cascading knock-on effects and trickle-down impacts. Making upgrades to a part of the system just amplifies the problem. There are delays adding them and pushing them out to a production environment where, especially in the need of security patches, they’re much needed.
Large systems on singular codebases like this are often called MONOLITHS. Our tiny product too, over the years, became THE MONOLITH.
Old Tech Stack
- A single code base for all the products, version-controlled by SVN.
- Custom made PHP Framework called FuseApp. Basically it’s just a router and nothing more.
- PHP 7 as both server-side and client-side, ie Core PHP & SMARTY as template engine. Also, over the period a lot of business logic crept into SMARTY files.
- No dev production parity, meaning we were using XAMPP on dev machines and a completely different set for production.
- No dependency management. We maintained the FOSS libraries we use.
- Tons of violations of design principles. No separation of concerns at all, business logic scattered all over in controllers, models, views, and database stored procs as well.
- As you can figure out by now, our system is a tightly integrated monolith, and hence change management and maintainability were a living hell.
We decided to decouple our application using the guidelines laid by The Twelve-Factor App, Martin Flower, and SOLIDprinciples. We decided to migrate organically ( Go Macro First, then Micro ) as it’s not feasible to migrate the entire 100 plus subproducts all at a once to the service layer. Our first task was to narrow down to that handful of mature and stable sub-products to conduct this change. Once that is figured out we,
- Codebase: Only one codebase per App, tracked in Git but many deploys. Deploy is a running instance of the app. This is typically a production site, and one or more staging sites. Additionally, every developer has a copy of the app running in their local development environment, each of which also qualifies as a deploy. The codebase is the same across all deploys, although different versions may be active in each deploy. For example, a developer has some commits not yet deployed to staging; staging has some commits not yet deployed to production. But they all share the same codebase, thus making them identifiable as different deploys of the same app.
- Upgraded from PHP 7 to PHP 7.2: We kept PHP for the application logic ( kudos for Our deep-rooted love for PHP ). We have migrated to PHP 7.2 mainly because it offers better performance and Libsodium as a Part of the Core.
- Migrated from FuseApp to Slim: Because of its tiny footprint, SLIM is one of the fastest frameworks bench-marked by various renowned authors. In addition to this, we can implement the route cache, which will improve the performance of big projects. Slim supports all native HTTP methods ( GET, POST, PUT, DELETE, etc.). The Slim framework uses PSR standards, and hence it’s extensible because we can replace the core components of slim with our custom ones or with any other vendors( which follows PSR standards ) and still our project will continue to work without any issues.
- Dev/prod parity: Our developers were using XAMPP on their Windows desktop. Now we have spun up a Vagrant box for them using the exact salt stack we use for production. So we have mirrored our production environments by providing the same operating system, packages, users, and configurations, all while giving developers the flexibility to use their favorite editor, IDE, and browser.
- Separation of config from code: We have separated App’s config i.e. everything that is likely to vary between deploys (staging, production, developer environments, etc) from code. This includes: Resource handles to the database, RabbitMQ, Credentials to external services such as Amazon S3, and other backing services. We store these configs on a separate repo where developers don’t have access to and we have configured our CI pipeline to inject the appropriate configs ( based on deploys – staging, production ) as environment variables to the server.
- APM: We use New Relic APM to retrospect our Apps performance and health.
- Treated logs as event streams: Our App never concerns itself with routing or storage of its output stream. We use Papertrail to aggregate all your logs (Syslog, Text log files, Windows events, Apache, etc ). No more digging through a dozen log files and directories.
- Enhanced Application Logic: We put Our Big Fat Controllers on the weight shedding program 😜 . Meaning after a lot of work out ( Thanks to SOLID principles ), we toned it down to super slim controllers. All the application businesses have been migrated to Business Objects backed by a Repository pattern for transacting with the database. Also, all the interaction with external systems got rewritten to RESTful API calls. We are using RabbitMQ for async processing, Redis for data caching, and avoiding sticky sessions. Our Apps by default are stateless and share-nothing. Any data that needs to be persisted is stored in a stateful backing service(PostgreSQL database ).
Our migration from a Monolith to SOA has been a great success. We want to stop here and wait for the appropriate use case and businesses need to move to microservices. Because migrating to microservices can end up in,
Author: Abin Thomas