Going SOA with Symfony2: A year and a half down the road
December 16, 2014 • Published by Javier Eguiluz
This Case Study is a guest post written by Alessandro Nadalin, VP Technology at Namshi. Want your company featured on the official Symfony blog? Send a proposal or case study to fabien.potencier@sensiolabs.com
When I first started working on Symfony2 to renew parts of our architecture at Namshi, I was genuinely excited because I was sure that Symfony was the best choice for our business. As time went by, I felt pretty confident about the choice, but I would like to give an honest and clear overview of which elements worked and which ones didn't, so that others can benefit from what we all learned from our experience.
Before we began
First, let me introduce our architecture, or, at least, how it looks nowadays: at Namshi, we run a fashion e-commerce portal that is growing rapidly in the Middle East (7 countries, 2 languages). In order to achieve desired growth, we tackled complexity by relying on microservices in a Service-Oriented Architecture.
We also diversified the development platforms, as our frontends are either "native" (mobile apps) or written in JavaScript, whether on NodeJS (desktop website) or on the client-side with AngularJS (mobile website).
As you might have already guessed, we use Symfony2 to develop the backend APIs used by those frontends. In this article, we'll focus solely on the backend part of the application.
To FOSRest or not to FOSRest?
If you are building APIs, you must use FOSRestBundle, right? Not so fast!
I'm not saying this isn't a good piece of software, but in highly customized, long-term projects you might want to look into something simpler, with less configuration and with your own constraints and domain rules.
We do use FOSRestBundle but have been slowly migrating away from it for all the new services that don't require too much abstraction: we determined that a simpler solution (i.e. JMS serializer + JsonResponse) is probably all we need.
When supporting additional formats, you might argue that using FOSRestBundle can save you a lot of work. That's why I think it´s important to evaluate your concrete needs and then opt for using it or not. In our case, we've seen that a simple method in the controllers was all we required:
class ApiController
{
public function createApiResponse($data, $whatever, ...)
{
return new JsonResponse($this->getSerializer()->serialize($data, 'json'), ...);
}
}
Again, I think FOSRest is a very nice piece of software. However, I would like to highlight the fact that you might be able to avoid additional complexity or abstraction if you don't need all the features.
Authentication
In order to authenticate API requests we decided to go for something that Google was promoting in some of its SDKs, Json Web Tokens.
We ended up writing a small library for using JWTs in PHP and, guess what, someone wrote a bundle for this. We're actually quite happy with the simplicity of JWTs so far and, compared with other more complex solutions, this simplicity has worked pretty well.
The idea behind JWS (Json Web Signatures, which are the signed version of JWTs) is very simple, as the client first logs in with credentials, then you issue a standard, signed JWT that contains client-related information and then the client sends that token for each API request.
Another important thing to note, all our APIs run on HTTPS and I would definitely recommend moving in that direction. With a faster and faster web, and things like SPDY and LTE connections, the perceived overhead is very small.
Multiple bundles or multiple apps?
Another thing that we discussed to great extent was whether to develop N Symfony apps or simply keep all our different APIs in isolated bundles, i.e. in one app.
We started with the latter since it was the fastest way to get stuff in production, but are currently moving towards splitting all of the services into their own apps. This strategy avoids getting trapped in dependency conflicts (one bundle needs version X of dependency Y that isn't supported in another bundle, for whatever reason, etc.) to keep the development process of these components separate.
This means that we can dedicate a small team of developers to work on one API without depending on any other API. In addition, the team can achieve fast, lean deployments without the need to deploy the whole API infrastructure at once. Lastly, this approach also helps in case we want to rewrite one of our services with another platform (NodeJS, for example).
Of course, these are problems that you start facing once you scale the team and scope of the software, so I would still recommend to start with multiple bundles and then simply split them in N apps only when necessary.
We also have some code / bundles that are common to basically every API and store it in separate repositories that will be included through Composer.
What about performance?
To be honest, nothing very specific here: we serve most of our content through Redis and have Varnish on top of Nginx, so that our API can relax once in a while.
Although we didn't attain crazy performance throughput, we are thrilled to see our APIs taking around 75ms, which is decent enough to not worry about performances for a while and focus on other aspects of the architecture.
Unit testing gotcha
Truth: we've been very remiss on this one. We´ve added functional tests without focusing too much on unit test; a trend that we've started to change recently.
In any case, there are a couple things I would like to mention:
- Instead of using PHPUnit, which is fully integrated out-of-the-box with Symfony, we decided to go for PHPSpec because we feel it's a more modern solution for unit tests and to let the software design emerge.
- Travis-CI is almost a no brainer: we've been using it for months now and are very happy not having to manage our own Jenkins instances to trigger tests every time someone pushes to GitHub.
CORS & friends
For those interested in knowing how we solved CORS issues, we started with the NelmioCorsBundle but, since we needed to support older versions of IE anyway and solutions like xDomain aren't as straightforward as you might think, we decided to implement an API proxy so that we would avoid cross-origin requests altogether. Instead of requesting api.example.com/users
, the frontend at example.com
will hit example.com/api/users
and a rule on the webserver will send that request to the API on the api.
subdomain.
Versioning
In an effort to break as little as possible, we embraced versioning and implemented it through the webserver and Symfony itself.
The approach we took is actually quite simple. We let clients hit our APIs through URLs like api.example.com/checkout/v3/...
. The trick being that the nginx server takes the first two segments of the URL and sets two HTTP headers accordingly:
Api-Service: checkout
Api-Version: 3
Then, we have a routing listener that boots controllers accordingly to the version: it checks if a controller is available under the \Namshi\CheckoutBundle\Controller\V3
namespace. If not, it fallbacks until it reaches version zero (\Namshi\CheckoutBundle\Controller
) or throws a 404
error.
On top of this, before sending the response back to the client, we have response transformers taking the stage: we look at the API version that was requested, find the transformers that match that version and apply them to the response.
The transformer is a simple POPO (Plain Old PHP Object) that accepts a Response
and applies transformations when necessary. For example, we use a transformer to camelCase values in the API responses, to add some caching rules or to trim content that we don't need on specific versions. Here´s how transformers look:
class CamelCaseTransformer implements ApiResponseTransformer
{
protected $inflector;
public function __construct(SomeInflector $inflector)
{
$this->inflector = $inflector;
}
public function transform(Response $response)
{
$content = $this->inflector->camelize($response->getContent());
$response->setContent($content);
}
}
All in all
The Tech team at Namshi is far from being perfect and I truly believe there's still much to do and many improvements to make. Still, some very simple choices, like the ones we described, helped us get ahead of many of our competitors due to the flexibility that comes from reducing the complexity of our Symfony-powered APIs.
We are very happy to be using Symfony2 for our APIs and are certain that, if we had to start again from scratch, we wouldn't go for anything else offered by the PHP ecosystem.
Please send us your feedback about the decisions we made, because we truly believe we can and should learn as much as possible from the smart brains in the Symfony community.
Last, but not least, in the process of writing our SOA we had the chance to develop some open source components (both in JavaScript and PHP) that are available on GitHub: feel free to drop by and help us make them even more awesome!
Help the Symfony project!
As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.
Comments are closed.
To ensure that comments stay relevant, they are closed for old posts.
We use JWT as well for our APIs thanks to the LexikJWTAuthenticationBundle and its dependencies (yours among them) and we are very happy with it, it's a really simple authentication system to implement.
And it was a perfect fit with the NelmioCorsBundle as our APIs are only used by a mobile app or from other server-side apps (no legacy browsers to take into account then).
There's two things I would say about bundles vs micro services. First I think user applications don't need to use bundles in symfony at all because... well, there's no real reason to use it, it makes your code coupled to symfony, it adds two unneeded directories in hierarchy (vendor and bundle names), there's some other reasons which I'll not remember right now.
And what about micro services - it's a very good way to distribute programmers work, to write decoupled code and it's basically isolation of components which usually good for testing and simplicity. But there's two problems you should always keep your eye on: complexity of dependencies between services - the more you divide everything the more complex it becomes for developers to grasp the architecture because everything is divided into small pieces and lies in different places, and the second thing is latencies between services that can add up and make your response time worse, they will often have a nature of sequential requests which you cannot "speedup" by asynchronicity.