Doctrine’s Second Level Cache in a Symfony Application

12 Apr, 2017
Xebia Background Header Wave

This article describes how to improve your Symfony app’s performance by introducing Doctrine’s second level cache. All conclusions were made after testing with Redis 3.0 and MySQL 5.7.

First Level Cache

With the new Symfony application and Doctrine, which is enabled by default, a very basic cache is made available in Symfony, which prevents having to query the database multiple times by searching for an entity by its primary key. It is very useful because it works out of the box and cannot be disabled, which means that it does not require any additional work.

The Traditional Approach

When using the traditional approach with a cache system such as Memcached, Redis or Varnish, we have to save data directly to the cache and read from it. We have full control of the cache, which gives us the possibility to improve and customise it, however, having full control can also generate a field for errors.

What Is a Second Level Cache?

Since at least January 2014, a second level cache has been “marked as experimental", however, it has been worked on since then (the latest stable tag of Doctrine’s ORM is quite different from the master regarding the second level cache).

Cached data is divided into three types:

  1. Entity data – as simple as it gets – entity data cached by its identifier. It works very well and it is actually the only type of data, which can be safely used in a production environment.
  2. Collection data – all entities are cached by a foreign key identifier (e.g. for the query SELECT * FROM city WHERE country_id = 3). This generates two problems, both of which can easily be seen as deal-breakers.

We can choose two strategies to invalidate them:

  • Evict the entire cache collection. This is relatively easy to do after having updated the entity and having used the class metadata. Once we have done that, we get association mappings and for each of them we use targetEntity and inversedBy values, and henceforth, we are able to evict the complete cache collection. However, in many cases, this is an infeasible and unacceptable option because it invalidates a lot of the cache at once.
  • Evict the cache collection by filtering with the use of the updated entity. This strategy can limit entity associations only related to the original one, but similarly, if the collections are big, it might actually decrease performance.

Moreover, the Redis caching collection will store a list of IDs with one key and each entity will be stored separately. So assuming Redis 3.0 is ten times faster than MySQL 5.7, having a collection that contains more than 10 entities will decrease performance.

  1. Query data, which contains a cache of queries executed from custom repositories. Aside from facing similar problems as with the entire cache collection, we encounter a new one, namely, the second level cache does not support scalar results. This means that any queries regarding grouping and aggregate functions cannot be cached, which is a real letdown because they are usually the heaviest ones.

A second level cache introduces caching regions, it does not store entity data – it only stores its own identifier and values. Each type of data can be freely assigned to a fixed region – if we do not define a region it will be created for us "behind the scenes". We can define such a region and associate all of the database queries with a fixed entity connected to that region so that invalidating the cache will be limited to that region – although, this requires manual configuration. Each region can have its own lifetime, which we define and forget about.

Caching Modes and Persisters

The second level cache comes with three caching modes:

  • READ_ONLY – default; the fastest and simple but also unable to perform updates and locks
  • NONSTRICT_READ_WRITE – able to perform updates but not locks
  • READ_WRITE – the slowest one, able to perform updates and locks and the only we can use when implementing a custom region

How to Configure It

Let us start with installing the required libraries:


composer require doctrine/doctrine-bundle doctrine/orm snc/redis-bundle predis/predis


We have to enable the second level cache:

# app/config.yml
enabled: true

We can also define a region here:

# app/config.yml
lifetime: 86400 # 1 day
type: service
id: snc_second_level_cache

We have to create a cache provider for it as a service – e.g. Predis:

# app/services.yml
class: '%snc_redis.doctrine_cache_predis.class%'
- '@snc_redis.default'

All that is left now is to configure entities and collections:

# src/AppBundle/Resources/config/doctrine/MyEntity.orm.yml
usage: NONSTRICT_READ_WRITE # - this one seems to be the safest choice
region: entity_that_rarely_changes # - this is optional, if we won't define a region name one will be generated from a class name
# ...

And queries:


class MyEntityRepository extends EntityRepository
public function getEvenOnes()
return $this->createQueryBuilder('MyEntity')
->andWhere("mod(,2) = 0")



  • Setting up a second level cache is very easy and the supporting libraries are very stable.
  • If our application often gets entities by their identifier, we can gain quite a lot from the second level cache.
  • Caching collections or queries may quickly become very inefficient – we would need a very specific application to benefit from it.
  • The second level cache seems to have a huge potential development speed, which raises the question as to whether it will be beneficial in Symfony projects.

Get in touch with us to learn more about the subject and related solutions

Explore related posts