spring-modulith
spring-modulith copied to clipboard
Support Event Publication Registry with multiple TransactionManagers (separate PostgreSQL schemas)
I have an application with two @ApplicationModule: orders and products. I want to have a separate PostgreSQL schema for each of them using spring-boot-starter-data-jpa. That's how I achieved the desired configuration.
TransactionManagers setup:
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.modularmonolithexample.products.domain"}, entityManagerFactoryRef =
"productsEntityManagerFactory", transactionManagerRef = "productsTransactionManager")
@EntityScan("com.modularmonolithexample.products.domain.*")
public class ProductsPersistenceConfiguration {
@Bean
@ConfigurationProperties("spring.datasource.products")
public DataSourceProperties productsDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource productsDataSource() {
return productsDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean productsEntityManagerFactory(@Qualifier("productsDataSource") DataSource dataSource,
EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource).packages("com.modularmonolithexample.products.domain").build();
}
@Bean
@Qualifier("productsTransactionManager")
public PlatformTransactionManager productsTransactionManager(
@Qualifier("productsEntityManagerFactory") LocalContainerEntityManagerFactoryBean productsEntityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(productsEntityManagerFactory.getObject()));
}
}
and
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.modularmonolithexample.orders.domain"}, entityManagerFactoryRef =
"ordersEntityManagerFactory", transactionManagerRef = "ordersTransactionManager")
@EntityScan("com.modularmonolithexample.orders.domain")
public class OrdersPersistenceConfiguration {
@Bean
@ConfigurationProperties("spring.datasource.orders")
public DataSourceProperties ordersDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource ordersDataSource() {
return ordersDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean ordersEntityManagerFactory(@Qualifier("ordersDataSource") DataSource dataSource,
EntityManagerFactoryBuilder builder) {
return builder.dataSource(dataSource).packages("com.modularmonolithexample.orders.domain").build();
}
@Bean
@Qualifier("ordersTransactionManager")
public PlatformTransactionManager ordersTransactionManager(
@Qualifier("ordersEntityManagerFactory") LocalContainerEntityManagerFactoryBean ordersEntityManagerFactory) {
return new JpaTransactionManager(Objects.requireNonNull(ordersEntityManagerFactory.getObject()));
}
}
I configure them with the following properties file:
spring:
datasource:
orders:
url: jdbc:postgresql://localhost:5432/modular-monolith-example?currentSchema=orders
username: postgres
password: admin
products:
url: jdbc:postgresql://localhost:5432/modular-monolith-example?currentSchema=products
username: postgres
password: admin
With the above setup application (without Event Publication Registry configured yet) works correctly.
I added spring-modulith-starter-jpa, made use of @TransactionalEventListere and created the event_publication table according to the documentation in both schemas - the goal is to have separate event_publication table for every module db schema.
When I tested it, I was getting the following error when dispatching an event using ApplicationEventPublisher:
No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: ordersTransactionManager,productsTransactionManager
As I understand, this is cause by #JpaEventPublicationConfiguration#jpaEventPublicationRepository accepts EntityManager em without any @Qualifier and my setup doesn't provide one.
So I tried to add @Primary to one of the qualified TransacitonManagers (this is against the goal above, but at least I want to try it that helps), but then I was getting another error:
Unable to locate persister: org.springframework.modulith.events.jpa.JpaEventPublication
which looks similar to #345. However, when I try to follow the suggestion and add org.springframework.modulith.events.jpa to @EntityScan like this:
@EntityScan({"com.modularmonolithexample.products.domain.*", "org.springframework.modulith.events.jpa"})
It doesn't help in my case.
A also tried using spring-modulith-starter-jdbc insted instead. At the beginning it was throwing the same No qualifying bean exception. I tried again to add @Primary to one of the TransactionManagers. But the the result was quite strange - events were stored in orders.event_publication regardless of on which TransactionManager bean I was putting @Primary (🤯).
At this point I am running out of ideas... Thanks for any help!
EDIT
After a while I tried with the default DataSource and TransactionManager (autoconfigured by Spring) and I defined the schema in @Table annotation. That worked, however with this I had to define single event_publication in public schema. Still have no workaround for working with separate TransactionManagers
the goal is to have separate
event_publicationtable for every module db schema.
That has been a non-goal so far. I wonder how / whether this would work reasonably well in practice. Primarily because the listener decoration would need to know which schema to use to mark the publication completed. This logically ties the two transactions anyway (the publishing one and the consuming one). What is supposed to happen if the publishing transaction is running against the DataSource for schema one, but a listener is declared to run on schema two?
Is it an option to rather use one DataSource and assign the individual entities to dedicated schemas via @Table(schema = …)?
I see the @Table annotation works for you. You should be able to override the schema for the EventPublication JPA entity in orm.xml. In JPA we have little influence on any mapping customizations that you might want to apply. I wonder if it makes sense to add a property to the JDBC implementation that'd allow defining a custom schema to be used for that table.
I see the
@Tableannotation works for you.
Yes, at the end having EventPublication in a separate (in my case public) schema actually makes even more sense, since if I compared these modules to microservices and using a event store, it would be a single one for all microservices.
However I still see a use case of having multiple DataSource. One can read in many places about having separate database (or schemas) for different microservices. To preserve strong boundaries in modulith I would like to follow the same, hence multiple DataSource would be helpful. However:
What is supposed to happen if the publishing transaction is running against the DataSource for schema one, but a listener is declared to run on schema two?
As far as I understand, if EventPublication is in separate schema/database it can't be tight to a particular transaction for neither publisher or consumer.
Would there be a solution/workaround for this scenario?
In this context, schema literally means “set of tables”. If you achieve that separation for individual modules, you're set. Using separate DataSources doesn't work, as transactions are scoped to individual ones by definition (via the Connection obtained from them).
It would be nice if you could give the latest snapshots a try, as they contain a fix for GH-685, that allows you to define the schema of the event publications table explicitly.
@odrotbohm Sorry for reanimating closed ticket, but I didn't managed to respond in time :)
GH-685 looks fine.
But there is still a use case that bothers me - separate databases. Using different schemas for different modules allows having single DataSource and satisfies my separation concerns. But what if (for some, hypothetical reason) there are separate db instances I need to work with in my modules?
I know that separate databases imply separate DataSources, and separate DataSources imply separate transaction managers. Probably it would require a totally different approach e.g. persisting the event publication inside a DataSource related to a transaction, in which the event is dispatched? But then it couldn't be bound to the transaction, that is consuming the event, as it can be under a different transaction manager? I don't know...
Anyway, maybe just to confirm - is there any way or any future plan to support working with multiple db instances? I understand if not - just want to have a clear understanding in case coming across this situation.
But what if (for some, hypothetical reason) there are separate db instances I need to work with in my modules?
The using Spring's local transaction management doesn't work in the first place. Spring Modulith doesn't change that. We do target that scenario, and it's unlikely we're ever going to. I'd suggest to look into Change Data Capture technology like Debezium to propagate changes from one data store into another.
Ok, fair enough.
Just to clarify, when I mention separate db instances I mean using separate db instance per module instead of separate schemas of the same db instance per module.
The bottom line for me - Spring Modulith event publication was not meant to support scenarios with separate db instances per module. Thanks for clarification!