Business:
As the diagram below shows, suppose we receive a requirement: listen to MySQL data changes via Canal, and sync those changes to other systems in real time — like Elasticsearch, MQ messages, or triggering business logic. What I’m trying to show is a solid, extensible base built with the right design patterns and strong open-closed compliance for all these scenarios.

design:
In this project, Spring Boot will definitely have a Listener as the main entry point. As soon as Canal detects even the slightest change in MySQL, we’ll be able to catch it in this Listener and take the appropriate action based on the event. Here’s the pseudocode:
/**
Data change monitoring business
* @param beforeColumns Data before the update
* @param afterColumns Data after the update
* @param tableName Table name
* @param eventType Operation type [insert, update, delete]
* @param timestamp Timestamp when the data changed
*/
public class CanalListener {
public void listen(List<CanalEntry.Column> beforeColumns, List<CanalEntry.Column> afterColumns, String tableName, CanalEntry.EventType eventType, long timestamp) {
if("user_info".equals(tableName)) {
if (CanalEntry.EventType.INSERT.equals(eventType)) {
//todo for insert
} else if (CanalEntry.EventType.UPDATE.equals(eventType)) {
//todo for update
} else if (CanalEntry.EventType.DELETE.equals(eventType)) {
//todo for delete
}
}
}
}
After reading it, you’ll notice a design problem: right now, we’re only handling the user_info table. But what if later iterations require listening to the order table too — do we just keep stacking logic on top? What if, three years later, there are another ten or twenty tables being maintained here? You’d end up with a situation where there are tons of ifs, ifs inside ifs, and business logic buried inside those ifs. By then, the code would definitely be “profound and great” — as the saying goes, “the ‘if’ gives birth to one, one gives birth to two, two gives birth to three, and three gives birth to all things,” and those “all things” are basically a crematorium.
So, instead of piling everything into the same place, it’s better to encapsulate a factory. The factory can give us a table-specific handler based on the table name. Inside the handler, we have methods for handling update, insert, and delete events. Then the Listener can be written like this (optimized pseudocode):
/**
* Data change listener logic
* @param beforeColumns The data before the update
* @param afterColumns The data after the update
* @param tableName The name of the table
* @param eventType The operation type [insert, update, delete]
* @param timestamp The timestamp when the data change occurred
*/
public class CanalListener {
public void listen(List<CanalEntry.Column> beforeColumns, List<CanalEntry.Column> afterColumns, String tableName, CanalEntry.EventType eventType, long timestamp) {
//获取处理器
TableHandler handler = TableHandlerFactory.getHandler(tableName);
if (ObjectUtil.isEmpty(handler)) return;
//执行各自的业务
ListenEntity listenEntity = new ListenEntity(afterColumns, beforeColumns, timestamp); //监听到的数据对象
if (CanalEntry.EventType.INSERT.equals(eventType)) {
handler.insert(listenEntity);
} else if (CanalEntry.EventType.UPDATE.equals(eventType)) {
handler.update(listenEntity);
} else if (CanalEntry.EventType.DELETE.equals(eventType)) {
handler.delete(listenEntity);
}
}
}
The core listener code is this small — short and clean. If you want to add a new table to listen to, you just implement the insert, update, and delete template methods based on TableHandler, and register it with the factory.
Here’s the implementation approach for the factory and handlers. First, the factory maintains a HashMap. When you get a handler from the factory by table name, it’s actually pulling it from the HashMap. Registering a handler is just a put operation on the HashMap.
// 工厂
public class TableHandlerFactory {
public static Map<String, TableHandler> tableNameHandlerMap = new HashMap();
// 把表handler注册到TableHandlerFactory中
public static void register(String tableName, TableHandler tableHandler) {
if (StrUtil.isEmpty(tableName)) {
throw new IllegalArgumentException("Registered tableName is empty");
} else if (ObjectUtil.isEmpty(tableHandler)) {
throw new IllegalArgumentException("Registered tableHandler is empty");
}
tableNameHandlerMap.put(tableName, tableHandler);
}
// 根据表名获取处理器
public static TableHandler getHandler(String tableName) {
if (StrUtil.isEmpty(tableName)) {
throw new IllegalArgumentException("illegal input for get tableHandler,tableName is empty");
}
return tableNameHandlerMap.get(tableName);
}
}
Why did I use an abstract class for TableHandler instead of an interface? The benefit is that an abstract class can provide default implementations. If a handler doesn’t implement a certain method from the abstract class — say delete — but delete gets called in the business logic, it will throw an UnsupportedOperationException by default. Alternatively, we could also throw a RuntimeException("This method has not been implemented");
public abstract class TableHandler implements InitializingBean {
public void insert(ListenEntity listenEntity) {
throw new UnsupportedOperationException();
}
public void update(ListenEntity listenEntity) {
throw new UnsupportedOperationException();
}
public void delete(ListenEntity listenEntity) {
throw new UnsupportedOperationException();
}
}
Each table’s business logic is encapsulated in its handler.
@Component
public class OrderTableHandler extends TableHandler {
private String tableName = "t_order";
@Override
public void insert(ListenEntity listenEntity) {
//todo
}
@Override
public void update(ListenEntity listenEntity) {
//todo
}
@Override
public void delete(ListenEntity listenEntity) {
//todo
}
// Spring启动时注册自己到Factory工厂
@Override
public void afterPropertiesSet() throws Exception {
TableHandlerFactory.register(tableName, this);
}
}
Surprise! that’s it.
Honestly, I can’t even clearly name which design patterns I used here. The way I see it, design patterns are about using the features of your language well — interfaces, abstract classes, regular classes, any kind of component — and combining them in a way that makes the business foundation work like magic. That’s my personal take on design. And design is flexible — the same thing can be designed in so many different ways, and everyone has their own style, so the implementation I showed is just one of many options.
Anyway, no brute force.