dao-factory/README.md
The DAO Factory combines the Data Access Object and Abstract Factory patterns to seperate business logic from data access logic, while increasing flexibility when switching between different data sources.
Real-world example
A real-world analogy for the DAO Factory pattern is a multilingual customer service center. Imagine a bank that serves customers speaking different languages—English, French, and Spanish. When a customer calls, an automated system first detects the customer's preferred language, then routes the call to the appropriate support team that speaks that language. Each team follows the same company policies (standard procedures), but handles interactions in a language-specific way.
In the same way, the DAO Factory pattern uses a factory to determine the correct set of DAO implementations based on the data source (e.g., MySQL, MongoDB). Each DAO factory returns a group of DAOs tailored to a specific data source, all conforming to the same interfaces. This allows the application to interact with any supported database in a consistent manner, without changing the business logic—just like how the customer service system handles multiple languages while following the same support protocols.
In plain words
The DAO Factory pattern abstracts the creation of Data Access Objects (DAOs), allowing you to request a specific DAO from a central factory without worrying about its underlying implementation. This makes the code easier to maintain and flexible to change, especially when switching between databases or storage mechanisms.
Wikipedia says
The Data Access Object (DAO) design pattern is a structural pattern that provides an abstract interface to some type of database or other persistence mechanism. By mapping application calls to the persistence layer, the DAO provides some specific data operations without exposing details of the database. The DAO Factory is an extension of this concept, responsible for generating the required DAO implementations.
Class diagram
In this example, the persistence object represents a Customer.
We are considering a flexible storage strategy where the application should be able to work with three different types of data sources: an H2 in-memory relational database (RDBMS), a MongoDB (object-oriented database), and a JSON flat file (flat file storage).
public enum DataSourceType {
H2,
Mongo,
FlatFile
}
First, we define a Customer class that will be persisted in different storage systems. The ID field is generic to maintain compatibility with both relational and object-oriented databases.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Customer<T> implements Serializable {
private T id;
private String name;
}
Next, we define a CustomerDAO interface that outlines the standard CRUD operations on the Customer model. This interface will have three concrete implementations, each corresponding to a specific data source: H2 in-memory database, MongoDB, and JSON file.
public interface CustomerDAO<T> {
void save(Customer<T> customer);
void update(Customer<T> customer);
void delete(T id);
List<Customer<T>> findAll();
Optional<Customer<T>> findById(T id);
}
Here is the implementations
@Slf4j
@RequiredArgsConstructor
public class H2CustomerDAO implements CustomerDAO<Long> {
private final DataSource dataSource;
private final String INSERT_CUSTOMER = "INSERT INTO customer(id, name) VALUES (?, ?)";
private final String UPDATE_CUSTOMER = "UPDATE customer SET name = ? WHERE id = ?";
private final String DELETE_CUSTOMER = "DELETE FROM customer WHERE id = ?";
private final String SELECT_CUSTOMER_BY_ID = "SELECT * FROM customer WHERE id= ?";
private final String SELECT_ALL_CUSTOMERS = "SELECT * FROM customer";
private final String CREATE_SCHEMA =
"CREATE TABLE IF NOT EXISTS customer (id BIGINT PRIMARY KEY, name VARCHAR(255))";
private final String DROP_SCHEMA = "DROP TABLE IF EXISTS customer";
@Override
public void save(Customer<Long> customer) {
// Implement operation save for H2
}
@Override
public void update(Customer<Long> customer) {
// Implement operation save for H2
}
@Override
public void delete(Long id) {
// Implement operation delete for H2
}
@Override
public List<Customer<Long>> findAll() {
// Implement operation find all for H2
}
@Override
public Optional<Customer<Long>> findById(Long id) {
// Implement operation find by id for H2
}
}
@Slf4j
@RequiredArgsConstructor
public class MongoCustomerDAO implements CustomerDAO<ObjectId> {
private final MongoCollection<Document> customerCollection;
// Implement CRUD operation with MongoDB data source
}
@Slf4j
@RequiredArgsConstructor
public class FlatFileCustomerDAO implements CustomerDAO<Long> {
private final Path filePath;
private final Gson gson;
Type customerListType = new TypeToken<List<Customer<Long>>>() {
}.getType();
// Implement CRUD operation with Flat file data source
}
After that, we create an abstract class DAOFactory that defines two key methods: a static method getDataSource() and an abstract method createCustomerDAO().
The getDataSource() method is a factory selector—it returns a concrete DAOFactory instance based on the type of data source requested.
Each subclass of DAOFactory will implement the createCustomerDAO() method to provide the corresponding CustomerDAO implementation.
public abstract class DAOFactory {
public static DAOFactory getDataSource(DataSourceType dataSourceType) {
return switch (dataSourceType) {
case H2 -> new H2DataSourceFactory();
case Mongo -> new MongoDataSourceFactory();
case FlatFile -> new FlatFileDataSourceFactory();
};
}
public abstract CustomerDAO createCustomerDAO();
}
We then implement three specific factory classes:
H2DataSourceFactory for H2 in-memory RDBMS
public class H2DataSourceFactory extends DAOFactory {
private final String DB_URL = "jdbc:h2:~/test";
private final String USER = "sa";
private final String PASS = "";
@Override
public CustomerDAO createCustomerDAO() {
return new H2CustomerDAO(createDataSource());
}
private DataSource createDataSource() {
var dataSource = new JdbcDataSource();
dataSource.setURL(DB_URL);
dataSource.setUser(USER);
dataSource.setPassword(PASS);
return dataSource;
}
}
MongoDataSourceFactory for MongoDB
public class MongoDataSourceFactory extends DAOFactory {
private final String CONN_STR = "mongodb://localhost:27017/";
private final String DB_NAME = "dao_factory";
private final String COLLECTION_NAME = "customer";
@Override
public CustomerDAO createCustomerDAO() {
try {
MongoClient mongoClient = MongoClients.create(CONN_STR);
MongoDatabase database = mongoClient.getDatabase(DB_NAME);
MongoCollection<Document> customerCollection = database.getCollection(COLLECTION_NAME);
return new MongoCustomerDAO(customerCollection);
} catch (RuntimeException e) {
throw new RuntimeException("Error: " + e);
}
}
}
FlatFileDataSourceFactory for flat file storage using JSON
public class FlatFileDataSourceFactory extends DAOFactory {
private final String FILE_PATH = System.getProperty("user.home") + "/Desktop/customer.json";
@Override
public CustomerDAO createCustomerDAO() {
Path filePath = Paths.get(FILE_PATH);
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create();
return new FlatFileCustomerDAO(filePath, gson);
}
}
Finally, in the main function of client code, we will demonstrate CRUD operations on the Customer using three data source type.
// Perform CRUD H2 Database
LOGGER.debug("H2 - Create customer");
performCreateCustomer(customerDAO,
List.of(customerInmemory1, customerInmemory2, customerInmemory3));
LOGGER.debug("H2 - Update customer");
performUpdateCustomer(customerDAO, customerUpdateInmemory);
LOGGER.debug("H2 - Delete customer");
performDeleteCustomer(customerDAO, 3L);
deleteSchema(customerDAO);
// Perform CRUD MongoDb
daoFactory = DAOFactory.getDataSource(DataSourceType.Mongo);
customerDAO = daoFactory.createCustomerDAO();
LOGGER.debug("Mongo - Create customer");
performCreateCustomer(customerDAO, List.of(customer4, customer5));
LOGGER.debug("Mongo - Update customer");
performUpdateCustomer(customerDAO, customerUpdateMongo);
LOGGER.debug("Mongo - Delete customer");
performDeleteCustomer(customerDAO, idCustomerMongo2);
deleteSchema(customerDAO);
// Perform CRUD Flat file
daoFactory = DAOFactory.getDataSource(DataSourceType.FlatFile);
customerDAO = daoFactory.createCustomerDAO();
LOGGER.debug("Flat file - Create customer");
performCreateCustomer(customerDAO,
List.of(customerFlatFile1, customerFlatFile2, customerFlatFile3));
LOGGER.debug("Flat file - Update customer");
performUpdateCustomer(customerDAO, customerUpdateFlatFile);
LOGGER.debug("Flat file - Delete customer");
performDeleteCustomer(customerDAO, 3L);
deleteSchema(customerDAO);
The program output
17:17:24.368 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Create customer
17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Green)
17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red)
17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Blue)
17:17:24.514 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Update customer
17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Yellow)
17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red)
17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Blue)
17:17:24.573 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - H2 - Delete customer
17:17:24.632 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Yellow)
17:17:24.632 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Red)
17:17:24.747 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Create customer
17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca)
17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c2, name=Elliot)
17:17:24.834 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Update customer
17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca)
17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c2, name=Henry)
17:17:24.845 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Mongo - Delete customer
17:17:24.850 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=68173eb4c840286dbc2bc5c1, name=Masca)
17:17:24.876 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Create customer
17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Duc)
17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang)
17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Nhat)
17:17:24.895 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Update customer
17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Thanh)
17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang)
17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=3, name=Nhat)
17:17:24.897 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Flat file - Delete customer
17:17:24.898 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=1, name=Thanh)
17:17:24.898 [main] DEBUG c.i.d.App com.iluwatar.daofactory.App - Customer(id=2, name=Quang)
Use the DAO Factory Pattern when:
Benefits:
Trade-offs: