How MyBatis Leverages 9 Classic Design Patterns

This article explores how MyBatis implements nine fundamental design patterns—Builder, Factory, Singleton, Proxy, Composite, Template Method, Adapter, Decorator, and Iterator—by examining its source code, illustrating each pattern with explanations, diagrams, and code snippets to deepen developers' understanding of practical pattern usage.

Programmer DD
Programmer DD
Programmer DD
How MyBatis Leverages 9 Classic Design Patterns

Although we all know the 26 classic design patterns, most remain at the conceptual level and are rarely encountered in real development. MyBatis source code uses a large number of design patterns; reading the source and observing these patterns helps deepen understanding.

MyBatis employs at least the following design patterns:

Builder pattern – e.g., SqlSessionFactoryBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLStatementBuilder, CacheBuilder;

Factory pattern – e.g., SqlSessionFactory, ObjectFactory, MapperProxyFactory;

Singleton pattern – e.g., ErrorContext and LogFactory;

Proxy pattern – core of MyBatis such as MapperProxy, ConnectionLogger (uses JDK dynamic proxy); the executor.loader package also uses CGLIB or Javassist for lazy loading;

Composite pattern – e.g., SqlNode and its subclasses like ChooseSqlNode;

Template Method pattern – e.g., BaseExecutor, SimpleExecutor, and BaseTypeHandler with subclasses such as IntegerTypeHandler;

Adapter pattern – MyBatis Log interface and its adapters for JDBC, Log4j, etc.;

Decorator pattern – cache decorators in the cache.decorators package;

Iterator pattern – e.g., PropertyTokenizer implementation.

Below we interpret each pattern: first introduce the pattern itself, then explain how MyBatis applies it.

1. Builder Pattern

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is a creational pattern used when object construction is too complex for a constructor.

During MyBatis initialization, SqlSessionFactoryBuilder invokes XMLConfigBuilder to read all mybatis-config.xml and *Mapper.xml files, building the core Configuration object, which is then used to construct a SqlSessionFactory. XMLConfigBuilder also uses XMLMapperBuilder and XMLStatementBuilder to parse mapper files and SQL statements. All these builders read files, parse XML, perform reflection, and cache results—tasks unsuitable for a simple constructor, hence the Builder pattern.

Key methods in SqlSessionFactoryBuilder start with build*, for example:

2. Factory Pattern

MyBatis uses a simple factory pattern for objects like SqlSessionFactory. A static factory method creates instances based on input parameters, encapsulating object creation.

SqlSession

is the core MyBatis interface for executing SQL, obtaining mappers, and managing transactions, similar to a JDBC Connection. Its factory provides overloaded openSession methods supporting various parameters.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
            boolean autoCommit) {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

This method shows how MyBatis builds a SqlSession by obtaining a Transaction, creating an Executor, and finally constructing the session.

3. Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point. MyBatis uses it for ErrorContext (thread‑local singleton) and LogFactory (global singleton).

public final class LogFactory{
    private static Constructor<? extends Log> logConstructor;
    private LogFactory(){ }
    public static Log getLog(Class<?> aClass){
        return getLog(aClass.getName());
    }
}
ErrorContext

stores error information per thread:

public class ErrorContext{
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
    private ErrorContext(){ }
    public static ErrorContext instance(){
        ErrorContext context = LOCAL.get();
        if (context == null){
            context = new ErrorContext();
            LOCAL.set(context);
        }
        return context;
    }
}

4. Proxy Pattern

Proxy is the core of MyBatis: developers write only mapper interfaces, and MyBatis generates proxy objects that handle SQL execution.

Subject – abstract subject

Proxy – proxy subject

RealSubject – real subject

When Configuration.getMapper is called, MapperProxyFactory.newInstance creates a MapperProxy which implements InvocationHandler:

public class MapperProxyFactory<T>{
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
    public MapperProxyFactory(Class<T> mapperInterface){
        this.mapperInterface = mapperInterface;
    }
    public T newInstance(SqlSession sqlSession){
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
    protected T newInstance(MapperProxy<T> mapperProxy){
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }
}

The proxy’s invoke method delegates to the appropriate MapperMethod which eventually calls SqlSession and Executor to run the SQL.

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

5. Composite Pattern

The Composite pattern treats individual objects and compositions uniformly. In MyBatis, SqlNode and its subclasses form a tree representing SQL fragments.

Dynamic SQL is built by recursively applying each SqlNode. For example, TextSqlNode appends raw text, while IfSqlNode evaluates a condition before delegating to its child nodes.

public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}
public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
        contents.apply(context);
        return true;
    }
    return false;
}

6. Template Method Pattern

MyBatis’s BaseExecutor defines the skeleton of SQL execution, leaving specific steps to subclasses such as SimpleExecutor, ReuseExecutor, and BatchExecutor.

Abstract methods that subclasses implement include:

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
            ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

For example, SimpleExecutor creates a new Statement for each operation:

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.update(stmt);
    } finally {
        closeStatement(stmt);
    }
}

7. Adapter Pattern

The Adapter pattern converts one interface to another. MyBatis defines a Log interface and provides adapters for various logging frameworks (Log4j, SLF4J, etc.).

public interface Log {
    boolean isDebugEnabled();
    boolean isTraceEnabled();
    void error(String s, Throwable e);
    void error(String s);
    void debug(String s);
    void trace(String s);
    void warn(String s);
}

Implementation example for Log4j:

public class Log4jImpl implements Log {
    private static final String FQCN = Log4jImpl.class.getName();
    private Logger log;
    public Log4jImpl(String clazz){
        log = Logger.getLogger(clazz);
    }
    @Override public boolean isDebugEnabled(){ return log.isDebugEnabled(); }
    @Override public void error(String s, Throwable e){ log.log(FQCN, Level.ERROR, s, e); }
    // other methods delegate to the Log4j Logger
}

8. Decorator Pattern

MyBatis cache uses the Decorator pattern. The core Cache interface is implemented by PerpetualCache, which is then wrapped by decorators such as FifoCache, LruCache, LoggingCache, SerializedCache, ScheduledCache, SoftCache, SynchronizedCache, and WeakCache. A special TransactionalCache adds transaction support.

Cache hierarchy (outermost to innermost):

SynchronizedCache → LoggingCache → SerializedCache → ScheduledCache → LruCache → PerpetualCache

. MyBatis also distinguishes first‑level (local) cache stored in the executor and second‑level (global) cache stored in the Configuration.

9. Iterator Pattern

The Iterator pattern provides a way to access elements of a collection without exposing its internal structure. MyBatis’s PropertyTokenizer implements Iterator to parse property strings.

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
    private String name;
    private String indexedName;
    private String index;
    private String children;
    public PropertyTokenizer(String fullname){
        int delim = fullname.indexOf('.');
        if (delim > -1){
            name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            name = fullname;
            children = null;
        }
        indexedName = name;
        delim = name.indexOf('[');
        if (delim > -1){
            index = name.substring(delim + 1, name.length() - 1);
            name = name.substring(0, delim);
        }
    }
    public boolean hasNext(){ return children != null; }
    public PropertyTokenizer next(){ return new PropertyTokenizer(children); }
    public void remove(){ throw new UnsupportedOperationException("Remove is not supported"); }
}

These nine patterns illustrate how MyBatis combines classic design principles with practical implementation techniques to build a flexible, extensible persistence framework.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsJavaBackend DevelopmentMyBatis
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.