Service-ception: how and why we’ve built a service inside a service
The road to micro-services architecture: creating a service within a service
Here at Yotpo, we run a very high scale (tens of millions of requests per day) web system. We ship our product to eCommerce web sites, each with its own scale of shoppers, all of which we serve.
One of the products we offer our customers is User Generated Images (images created by shoppers). We recently started working on a new product offering — an Album Widget. In this widget, shop owners can choose images which their shoppers uploaded, and show these images on an onsite widget. Moreover, we will allow them to moderate and add images from instagram to the album.
The images, the album, and the album creation experience will all be developed and served by Yotpo. Obviously this product requires a lot of backend entities and code, which will live and engage with the rest of our existing code base.
In the following post we will try to describe how we kept a service oriented approach in the development process, while using the same code base and the same process. We will discuss what we did, why we did it, and the advantages and disadvantages we found for our approach along the way.
No one can ignore the crazy hype going around ‘micro-services’ architecture these days. Describing the architectural merits is beyond the scope of this post. Suffice to say that the few services we currently own have grown to massive proportions, and our goal as developers is to avoid adding any new code to them. Instead, it is preferred to write new, small services that are as decoupled as possible from existing services.
However, creating a new service has a considerable overhead effort for anyone. It means creating a whole new deployment process, repository, database, tooling chain and more. The time constraints on R&D projects are always very pressing, and product teams often have to surrender a lot of feature content to meet them.
Developers constantly seek to find a way to keep the long term service oriented strategy, while still maintaining a tight schedule.
The proposed solution
So how can this problem be confronted? For us, a mid-way solution was the way to go: expanding one of the existing services, the “Social media service” (used for fetching media from instagram).
But, this cannot be achieved effectively just by adding new controllers and models the good old way we know. It is important to make sure all the new behaviour is in a whole separate flow, as well as a separate folder structure. The new code addition should get a new name, and treated as a complete separate service. This separation might seem ‘semantic’ and imposed, but keeping it helps keep the new ‘service within a service’ as decoupled as possible from the existing service.
This solution keeps the service oriented architecture as its guiding principle, while avoiding most of the overhead effort tied with creating a new micro service.
During the development process we obviously faced some unforeseen challenges. The most interesting ones were:
The new (albums) service and the old (social media) service still need to communicate with each other.
For example, an album can contain images from the social service. Whenever there’s a request for an album, it will need to return data for its social images. That data exists in the social images service.
At this point it will be very tempting to mix the services. For example, use a model from the social media service inside one of the albums service’s controllers. To avoid that, we introduced a new entity which will prevent the coupling — SocialImagesProvider.
This entity will be the only point of communication from the albums service to the social images service. If and when we decide to extract one of the services into a separate microservice, the SocialImagesProvider will be the only entity needed to be reimplemented.
Communication to external services was also difficult. Just as an album can contain a social image (from instagram), it can contain a user generated image. So again, when there’s a request for an album containing a user generated image, the album service will need to fetch the image’s data. Only this time, the data isn’t in its brother-service, it’s in one of our other services.
This basically requires to perform an inter-service join, so as to get all the image’s details. This join is complicated, as it contains two services, and is not very reliable. We will need to reconsider this implementation very soon as we continue to scale our application.
There are many advantages to writing the new service inside the old one, amongst them:
- Using a single database: this saves the time of deploying and maintaining a new database. The tables for the new service are new tables that do not relate at all to the existing ones.
- Known and familiar code: the code base, architecture, database and more are all familiar to all the developers on the project, so it’s still possible to move quickly without learning a new programming languages/architecture/database.
- Inner-service communication: the two brother-services can communicate efficiently with other: they basically run on the same process.
- Easy to extract micro-service in the future: the new service is well defined and separated from the existing one, so it is (relatively) easy to extract it to a separate micro-service in the future.
- Decoupled: the new service is decoupled from the old one, so the two services can advance independently. Changes to one can be made without worrying to the other. Moreover, the service’s boundaries are very clear, so it’s kept small enough, and easier to fully grasp by developers.
- No ‘new service overhead’.
- Services have to be deployed together: even though a change to one service is independent of the other, deployment is still joint. If one of the services breaks, the other will break as well.
- Some coupling is inevitable.
- Project still grows larger: the original service still grows, the code base becomes larger and harder to fully grasp, and the database grows significantly.
In the post we have described our approach to developing a new meaningful feature, while trying to maintain a balance between a tight schedule and a new architectural approach — micro-services.
The path we chose — a service with a service — has advantages and disadvantages, just like any other path. The development and deployment went very smoothly, and the product is up and running to the satisfaction of our customers.
In our near future road map, the new service will be extracted, and given a life of its own, with a new database, deployment, and everything else that accompanies a decoupled micro-service.