Sometimes there’s the need to connect to different datasources from within one application at the same time. Those datasources may be of different type, but sometimes need to be all relational or in this case, all need to be a different Neo4j instance. From an architectural point of view there could be a good opportunity to introduce different services per datasource but this is not the scope of this article.

1. Problem

I want to use different Neo4j instances through different Neo4j-OGM session factories in my Spring Boot application. I have several domains, each with their own entity classes and repositories and each domain has it’s own Neo4j instance. I have added the Spring Boot Starter Data Neo4j but it allows only one connection and all my repositories use that.

2. Solution

For this to work you’ll need Spring Boot 2.x and the most recent release of Spring Data which is called Spring Data Lovelace (RC1).

In a standard Spring Boot 2 application generated at start.spring.io you would change the Spring Data Release train with the following property:

Listing 1. pom.xml
<properties>
        <spring-data-releasetrain.version>Lovelace-RELEASE</spring-data-releasetrain.version>
</properties>

Then the solution can be broken down into two problems:

  1. Provide a multiple connections to Neo4j by reusing the available Neo4jProperties used to configure Spring Data Neo4j in Spring Boot. That way you avoid duplicating existing properties just for a second connection.

  2. Configure Spring Data Neo4j to use the correct session factory.

2.1. Provide a multiple connections to Neo4j

The following code are in Domain1Config.java and Domain2Config.java of this articles example project using-multiple-session-factories.

We are dealing with the following configuration of our Spring Boot application

Listing 2. application.properties
# Configuration for entities and repositories in "domain1"
spring.data.neo4j.username = neo4j
spring.data.neo4j.password = domain1
spring.data.neo4j.uri = bolt://localhost:7687

# Configuration for entities and repositories in "domain2"
spring.data.neo4j.domain2.username = neo4j
spring.data.neo4j.domain2.password = domain2
spring.data.neo4j.domain2.uri = bolt://localhost:7688

As you see we’re using the default properties for our domain 1. The configuration for domain 2 looks very similar, but has an additional prefix inside their name. They are in fact both mapped to the same configuration class. How are we doing that?

Domain1Config does something that is normally done by the spring-boot-starter-data-neo4j, it creates an instance of Neo4jProperties:

Listing 3. Domain1Config.java
@Primary
@Bean
@ConfigurationProperties("spring.data.neo4j")
public Neo4jProperties neo4jPropertiesDomain1() {
        return new Neo4jProperties();
}

The property class uses @ConfigurationProperties(prefix = "spring.data.neo4j") which maps configuration values (either properties, environment, config server, whatever) to property beans. This property bean is marked as primary and given a name that is the same as the method name creating it (neo4jPropertiesDomain1).

With that property we’ll create the OGM configuration, session factory and transaction manager as needed for instance 1, pretty much as the starter normally does. The starter will back off from providing those beans if they already exists.

Listing 4. Domain1Config.java
@Primary
@Bean
public org.neo4j.ogm.config.Configuration ogmConfigurationDomain1() {
        return neo4jPropertiesDomain1().createConfiguration();
}

@Primary
@Bean(name = SESSION_FACTORY) (1)
public SessionFactory sessionFactory() {
        return new SessionFactory(ogmConfigurationDomain1(), BASE_PACKAGE); (2)
}

@Bean(name = TRANSACTION_MANAGER) (3)
public Neo4jTransactionManager neo4jTransactionManager() {
        return new Neo4jTransactionManager(sessionFactory());
}
1 Uses a static constant to provide a dedicated name for the session factory.
2 A static constant containing the fully qualified name of domain 1 java package.
3 Same as in 1 but for the transaction manager.

Now for the second instance. We’ll have a look at the full configuration class to get whole picture:

Listing 5. Domain2Config.java
@Configuration
@EnableNeo4jRepositories(
                sessionFactoryRef = SESSION_FACTORY,
                basePackages = BASE_PACKAGE,
                transactionManagerRef = TRANSACTION_MANAGER,
                sessionBeanName = SESSION_BEAN_NAME (1)
)
public class Domain2Config {

        public static final String SESSION_FACTORY = "sessionFactoryForDomain2";
        public static final String SESSION_BEAN_NAME = "aSessionToInstance2";
        public static final String TRANSACTION_MANAGER = "transactionManagerForDomain2";

        static final String BASE_PACKAGE = "org.neo4j.tips.sdn.using_multiple_session_factories.domain2";

        @Bean
        @ConfigurationProperties("spring.data.neo4j.domain2") (2)
        public Neo4jProperties neo4jPropertiesDomain2() {
                return new Neo4jProperties();
        }

        @Bean
        public org.neo4j.ogm.config.Configuration ogmConfigurationDomain2() {
                return neo4jPropertiesDomain2().createConfiguration();
        }

        @Bean(name = SESSION_FACTORY)
        public SessionFactory sessionFactory() {
                return new SessionFactory(ogmConfigurationDomain2(), BASE_PACKAGE);
        }

        @Bean(name = TRANSACTION_MANAGER)
        public Neo4jTransactionManager neo4jTransactionManager() {
                return new Neo4jTransactionManager(sessionFactory());
        }
}
1 Spring Data Neo4j has always provided an injectable, shared session, much like Spring Data JPA with the EntityManager. If there are two Neo4j-OGM session factories, there are two sessions as well. If you don’t provide a name as we did here, the session get’s qualified with the name of the session factory itself.
2 See below, we create another instance of Neo4jProperties.

Many people don’t know the fact, that one can use Spring Boots configuration properties mechanismen not only on classes, but also on @Bean-factory methods. It maps all properties from your configuration with the specified prefix to properties of the bean. In this case, the properties from Listing 2 are mapped to the bean. The rest of the configuration is - apart from different names - identically to the one for domain 1.

2.2. Configure Spring Data Neo4j to use the correct session factory

Having those connections in place we enable Neo4j repositories for "domain1" and "domain2" against the correct instances in our @Configuration classes as follows:

Listing 6. Domain1Config.java
@EnableNeo4jRepositories(
                sessionFactoryRef = SESSION_FACTORY, (1)
                basePackages = BASE_PACKAGE, (2)
                transactionManagerRef = TRANSACTION_MANAGER (3)
)
public class Domain1Config {
}
1 Specificies the Neo4j OGM Session factory to use instead the default (named sessionFactory which is no longer instantiated through the starter as we provided our own).
2 Use that session factory only for the given base package. Note that this is the same as when opening the session factory.
3 Also, use the correct transaction manager.

The repositories for domain 2 are configured in the same way. You see the configuration already in Listing 5.

2.3. How to use this?

2.3.1. On the Spring Data level

Given FooRepository.java and BarRepository.java in their respective Java-packages, you are using the different connections completely transparent as shown in Listing 7.

Listing 7. Domain2Config.java
@Service
public class DemoServiceUsingRepositories {

        private static final Logger LOGGER =
                LoggerFactory.getLogger(DemoServiceUsingRepositories.class);

        private final FooRepository fooRepository;

        private final BarRepository barRepository;

        public DemoServiceUsingRepositories(
                FooRepository fooRepository,
                BarRepository barRepository
        ) {

                this.fooRepository = fooRepository;
                this.barRepository = barRepository;
        }

        public void createSomeFooBar() {

                FooEntity fooEntity = fooRepository.save(new FooEntity("This is foo"));
                LOGGER.info("Written foo {} with id {}", fooEntity.getName(), fooEntity.getId());

                BarEntity barEntity = barRepository.save(new BarEntity("This is bar"));
                LOGGER.info("Written bar {} with id {}", barEntity.getName(), barEntity.getId());
        }
}

2.3.2. On the session level

You can use both the session factory or a session. They must be however qualified at their injection points as we have several matching beans of types SessionFactory and Session:

Listing 8. Domain2Config.java
@Service
public class DemoServiceUsingSession {

        private static final Logger LOGGER =
                LoggerFactory.getLogger(DemoServiceUsingSession.class);

        private final Session sessionToNeoInstance1;

        private final Session sessionToNeoInstance2;

        public DemoServiceUsingSession(
                        @Qualifier(Domain1Config.SESSION_FACTORY) (1)
                        Session sessionToNeoInstance1,
                        Session aSessionToInstance2 (2)
        ) {

                this.sessionToNeoInstance1 = sessionToNeoInstance1;
                this.sessionToNeoInstance2 = aSessionToInstance2;
        }

        public void readSomeFooBar() {

                this.sessionToNeoInstance1
                                .query(String.class, "MATCH (n) RETURN n.name", Map.of())
                                .forEach(LOGGER::info);
                this.sessionToNeoInstance2
                                .query(String.class, "MATCH (n) RETURN n.name", Map.of())
                                .forEach(LOGGER::info);
        }
}
1 Use the qualifier based on the session factories name
2 Use the name configured in Listing 5.

3. About the example

The example uses two server instances provided via Docker, so that you can convince yourself that the setup works as expected. For your convience, both instances can be started and stopped via a single Maven command (./mvnw docker:start`and `./mvnw docker:stop respectivly). You can access them at localhost:7474 and localhost:7475.

4. Added bonus: Spring Boots configuration processor

Applying @ConfigurationProperties to an @Bean method is subject to the generation of configurational metadata as well. Read about that topic here. In short: It’s enough to add spring-boot-configuration-processor as an optional dependency to your build

Listing 9. pom.xml
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
</dependency>

to generate metadata about your configuration properties (being completely your own or reused as we did). This metadata is read by any of the major IDEs (like NetBeans, IntelliJ IDEA or Spring Tool Suite), as shown in the following picture:

using multiple session factories properties support
Figure 1. Support of configuration metadata in IntelliJ IDEA