Domain Driven Architecture

From Opentaps Wiki
Revision as of 21:19, 9 July 2008 by Sichen (talk | contribs)
Jump to navigationJump to search

One problem with early versions of opentaps is that the ofbiz framework which we used is not an object-oriented framework. Instead, it is based on a data model which is fundamentally relational, and that data model is accessed via a map-like Java object called GenericValue. Most of the services in the business tier used a GenericDelegator to retrieve GenericValues from the database, performed operations on them, and then stored them back into the database again using the same GenericDelegator.

While this lightweight architecture could do a lot of things, as opentaps grew it became apparent that some of the application could significantly benefit from an object-oriented architecture. A few months ago, we started down this path and thought about how to write more object-oriented code with the ofbiz framework. More recently, after reading about Domain Driven Design and Domain Driven Design Quickly, we realized that what we really needed was not just object-oriented code, but rather a more formal classification of our business logic into domains. This document explains what domain driven architecture is, how we have implemented it, and how it could help you structure your code.

What is Domain Driven Design?

The basic idea behind a domain is to group together all the "domain expertise," or business knowledge, of an application and separate it from the application and its infrastructure. It is a different way of thinking about how to organize large software applications and complements the popular Model View Controller (MVC) architecture, which we also use in opentaps. With the Model View Controller architecture, the application's user interface (View) is separated from its business logic (Model), and a Controller directs requests from the view layer to the relevant business logic found in the model layer. The advantage of doing this is that the same business logic could then be reused elsewhere, either in another page in the view layer or as part of other business logic in the model layer.

MVC, however, doesn't really say how your model should be structured. Should it be object-oriented, or should it all be written in procedural languages or just SQL? Should they reside in separate components and packages, or could you just have one big file, which has all of your business logic? The domain driven design answers this question by separating the model layer ("M") of MVC into an application tier, a domain tier, and an infrastructure tier. The infrastructure tier is used to retrieve and store data. The domain tier is where the business knowledge or expertise is. The application tier is responsible for coordinating the infrastructure and domain tiers to make a useful application. Typically, it would use the infrastructure tier to obtain the data, consult the domain tier to see what should be done, and then use the infrastructure tier again to achieve the results.

For example, let's say that you wanted to assess late charges on all of your customers' outstanding invoices. MVC would tell you that your application should have a screen which shows you a list of outstanding invoices, and when the user says "Assess Late Charges", the controller would pass the users' input parameters to business logic in the model tier to do the dirty work of assessing those late charges.

With a domain driven design, we would look more deeply at what that dirty work actually involved. The application tier would call upon the infrastructure tier to retrieve all the invoices which may get assessed charges. Then, it would present that list of invoices to the domain tier, which has the business expertise to say "Should this invoice get charged?" and if so "How much should this invoice get charged?" The domain tier would then return the late charges for each invoice to the application tier. The application tier would then call on the infrastructure tier again to store the late charges into the database.

Why Domain Driven Design?

Why do we want to do all this?

You will be able to work with opentaps more easily

The first and most obvious benefit of domain driven design is that it helps us organize our application into natural domains, so you don't have to come in contact with all the 800+ tables in opentaps and the over 1,200 services that support them. For example, a domain driven design would allow us to break an application down into a few large domains, such as Customer, Order, and Invoice, and hide all the details within each of those domains from developers who don't need to work with them. Thus, if you are working on the Order domain, you may need to know a little bit about a Customer, such as his home address, shipping addresses, payment methods, but you don't really need to know all the tables used to track the relationship of customer information and their histories.

A related advantage is that it allows us to separate business tier expertise from infrastructure expertise. Thus, if you are working primarily with implementing business processes, you can write code which basically work with the different domains. You'll be happy to leave the database to somebody whose job is working on the infrastructure tier, and who's probably glad not to have to worry about your business processes.

You will be able to extend opentaps more easily

Imagine that you worked in an industry or a company that had customers, but they did some special things for their customers that most other companies don't. With an object-oriented domain driven design, you will be able to extend the existing Customer domain objects from opentaps with new methods specific to your industry or company, while still using everything from the opentaps Customer.

You will be able to use opentaps in novel ways

A potentially more valuable advantage is that domain driven design gets us closer to a plug-and-play application. Imagine again that your application is broken down into the Customer and Order domains, so that the Order domain interacts with customer information only through the Customer domain. What if you wanted to use the opentaps order entry and order management tool with another CRM application, like SugarCRM or SalesForce.com? With good domain separation, it would be a matter of just implementing the Customer domain objects used by the Order domain to call the new CRM application. Alternatively, if you wanted to use opentaps CRM with a legacy order management system, you could implement the Order domain objects used by the Customer domain in opentaps.

Finally, by separating out the domain tier of business knowledge from the infrastructure tier, it also allows us to deploy opentaps on a different infrastructure tier later as well. For example, instead of using the entity engine, you could use Hibernate or even the Google storage API instead. This frees your application from lock-in to a particular framework.

If these advantages sound familiar, they should be. They are in fact the advantages of encapsulation, polymorphism, and inheritance of object oriented programming. Domain driven design is essentially a practice for realizing those advantages in a large-scale application.

Terminology

Now let's look at some of the terminology used by Domain Driven Design, which will serve as our starting point:

  • Domain is a body of business expertise. For example, you might have a domain of all business expertise about customers -- who is responsible for them, what prices they should get, how to contact them, etc.
  • Entity is an object which has a distinct identity. For example, a Customer entity has a distinct identity with an ID.
  • Value Object is an object which has no distinct identity. For example, the color of a product does not have a distinct identity if you think the "blue" of two blue shirts are the same thing.
  • Aggregate is a higher level entity which could be viewed from the outside and in turn links you to other entities and value objects. For example, Customer might be an aggregate, so you can view Customer from Orders, Invoices, etc., but a Customer's addresses and phone numbers should only be retrieved by going through Customer first.
  • Infrastructure is where the lower level infrastructure of your application is available. For example, it would provide you with the ability to access databases, remote web services, etc.
  • Factory is used to create Entities. For example, a Factory might create an Invoice entity (and its related entities and value objects) from an Order entity.
  • Repository is used to retrieve, store, and delete Entities from the database. For example, a Repository might help you store the Invoice (and related entities) your Factory created and then bring them back from the database.
  • Service is business logic that involves several domain Entities or Aggregates. For example, creating Invoices from Orders is a service.

How Domain Driven Design is Implemented

When we started to implement the domain driven design, we faced a common issue for many developers: How could we need true to the spirit of a domain driven design, but at the same time live with our existing framework and code base?

What we did is first implement a set of foundation classes in org.opentaps.foundation.* to support the Entity, Repository, Inrastructure, Factory, and Service concepts under the ofbiz framework. For each of these, we implemented an interface, and then we implemented a specific version for the ofbiz framework. Thus, we have the following interfaces:

* org.opentaps.foundation.entity.EntityInterface
* org.opentaps.foundation.repository.RepositoryInterface
* org.opentaps.foundation.factory.FactoryInterface
* org.opentaps.foundation.infrastructure.Infrastructure
* org.opentaps.foundation.service.ServiceInterface

Then, for each of these we implemented a version for the ofbiz framework:

* org.opentaps.foundation.entity.ofbiz.Entity
* org.opentaps.foundation.repository.ofbiz.Repository
* org.opentaps.foundation.factory.ofbiz.Factory
* org.opentaps.foundation.infrastructure.ofbiz.Infrastructure
* org.opentaps.foundation.service.Service

Each of these is designed to map legacy code from ofbiz and ofbiz-based portions of opentaps into the concepts of the domain driven design:

Entity

The Entity object is designed to create an object equivalent to the ofbiz GenericValue. You can create an Entity from an ofbiz GenericValue by simply passing it to the Entity constructor:

  GenericValue invoiceValue = delegator.findByPrimaryKey("Invoice", UtilMisc.toMap("invoiceId", "10000"));
  Entity invoiceObject = new Entity(invoiceValue);

At this point, you can access all of the methods of the GenericValue, because Entity actually extends GenericValue! In fact, legacy code would actually think that your Entity object is a GenericValue. However, you can also define additional methods in your Entity object, extend your Entity class with subclasses, and get all the benefits of a real Java object. The Entity object is meant to be extended, so more entity specific methods could be added in the subclasses. For example, Entity is extended to Invoice, Order, and Party, the last of which is also further extended to Organization, Supplier, and Customer, which is further extended to Account, Lead, and Contact.

For example, our Invoice domain aggregate is implemented as an extension of Entity:

 public class Invoice extends Entity {
 ....

 public BigDecimal getInvoiceTotal() {
   ...
 }

With this implementation, you can access fields of the Invoice GenericValue or the object methods in the Invoice Entity object:

 String invoiceId = invoice.getString("invoiceId");
 Timestamp invoiceDate = invoice.getTimestamp("invoiceDate");
 BigDecimal invoiceTotal = invoice.getInvoiceTotal();

You can even use the .getRelated(...) and .store() methods of the Invoice GenericValue in your Invoice Entity object to access related entities or persist your Entity objects. Please don't do it -- that is something for the Repository.

What's In a Name? If you're familiar with ofbiz, you'll notice that it has something called GenericEntity versus our Entity object. Why are the names similar? Because they both model the same thing but in different ways. An "entity" is a common concept that refers to an object with a distinct identity. The ofbiz GenericEntity models all entities as generics, with a generic set of methods like .set(), .getString(..), etc. The opentaps Entity object extends that GenericEntity, so you can have both the generic methods and methods specific to your entity, like .getInvoiceTotal(). Thus, the ofbiz GenericEntity says "All entities are generic, and these are the generic methods for them." The opentaps Entity object says "All entities are in some ways generic and in some ways unique. You decide what's generic and what's unique."

Infrastructure

The Infrastructure class is designed to encapsulate the infrastructure of the application. The idea is that the Infrastructure is passed to the Repository and the Factory classes so that they can interact with the database and external Web services. Infrastructure is not meant to be extended. Rather, the InfrastructureInterface is intended to be a placeholder for the infrastructure to be passed to the Repository and the Factory classes. In the case of the ofbiz framework that is primarily the service dispatcher, from which you can obtain the delegator and security objects. The ofbiz Infrastructure also holds a system user login, in case Repositories and Factories need it.

Factory

The Factory class is designed to create Entity objects based on other parameters. For example, you might want to create an Invoice Entity based on customer and invoice terms, or you might want to create an Invoice Entity based on an existing Order, taking its customer and list of items as a starting point. The Factory class is meant to be extended to create Factories for the different domain aggregates such as Invoice, Customer, Order, etc.

For the ofbiz framework, the Factory often references legacy services. Note that this is an interesting issue: in a classic domain driven design, the Factory would create the Entity as pure objects, and then the Repository would be responsible for storing them to the database. Such separation of roles is not present in the ofbiz framework, however, where virtually every service would access the database, create new data, and then store it back into the database. Thus, to reuse these existing services, our Factories sometimes end up storing the objects to the database first by calling an ofbiz service, then retrieve them again and return them as Entity objects.

Repository

The Repository is designed to help retrieve and store Entities and is meant to be extended for the major Entities, so the foundation Repository should be extended to CustomerRepository, InvoiceRepository, and OrderRepository to support Customer, Invoice, and Order Entities.

For the ofbiz framework, the preferred way to retrieve and store data could either use the service dispatcher or the delegator. Therefore, the Repository could be instantiated either with the delegator alone or with the delegator, dispatcher, and user login. The Repository should offer a set of methods for retrieving or persisting its related Entity and then either use the delegator or call the service to do it.

Repositories or Factories?

Since almost all legacy ofbiz services store values into the database, the implementation of business logic as factories or repositories is more of a style choice. Remember that Factories are intended to create new Entity objects, while Repositories are intended to retrieve and store them. Therefore, we would follow the following rules for Factories and Repositories:

  1. Use Factories for create and Repositories for get and store
  2. Always return the domain's Entity object from your Factory, so it looks like a real object Factory
  3. Factories will almost always use the service dispatcher, whereas Repositories will usually use the dispatcher but may sometimes use the delegator

Services

Services are designed to encapsulate business logic that span multiple Entities, such creating Invoice from an Order. With the opentaps Service foundation, you can create your services as plain Java objects (POJOs), similar to the Spring framework or JBoss Seam. When your services object is instantiated, it will be created with Infrastructure, a User object, and the locale. Parameters for your service are passed into your service via set methods, the execution of your services via a void method, errors are propagated by exceptions, and the results of your service are passed back via get methods. This is in contrast to the ofbiz framework, where services are defined as static Java methods (don't ever write one in minilang!), the parameters are passed in a map, the results and any error messages are returned in a map.

Because these services are Java objects, we follow the convention to group services with similar parameters together into one class. For example, all services which create Invoices from Order should be in one class, and all services which create Invoices from Shipment should be in another. This allows them to share set and get methods without having one service class which is too long.

What domain should a service be a part of? By convention, we recommend that the service is part of the domain of its output, so all services which create Invoices should be part of the Invoice domain, whether the invoices are created from orders, shipments, or recurring agreements.

Finally, note that the Service implementation for the ofbiz framework still has getDispatcher() and getDelegator(), so you can access other services in the framework if you want to.

Exceptions

All exceptions should be implemented as subclasses of org.opentaps.foundation.exceptions.FoundationException, which is a base class that in its turn extends the ofbiz GeneralException class. EntityException, ServiceException, RepositoryException, InfrastructureException, and FactoryException all extend the base FoundationException class. You can in turn implement specific exceptions which subclass these general exceptions.

An Example Using Domains

Now let's consider an example. Suppose we want to create an invoice for all the order items which are not physical products and which have been marked as performed (See Fulfilling Orders for Services.) Using the ofbiz framework, we would first define a service:

    <service name="opentaps.invoiceNonPhysicalOrderItems" engine="java"
        location="com.opensourcestrategies.financials.invoice.InvoiceServices" invoke="invoiceNonPhysicalOrderItems">
        <description>Creates an invoice from the non-physical items on the order.  It will invoice from the status in the orderItemStatusId,
        or if it is not supplied, default to ITEM_PERFORMED.  After the invoice is created, it will attempt to change the items' status
        to ITEM_COMPLETE.</description>
        <attribute name="orderId" type="String" mode="IN" optional="false"/>
        <attribute name="orderItemStatusId" type="String" mode="IN" optional="true"/>
        <attribute name="invoiceId" type="String" mode="OUT" optional="false"/>
    </service>

Then, we would create a static Java method for the service:

    public static Map invoiceNonPhysicalOrderItems(DispatchContext dctx, Map context) {
        LocalDispatcher dispatcher = dctx.getDispatcher();
        GenericValue userLogin = (GenericValue) context.get("userLogin");
        Locale locale = (Locale) context.get("locale");

        String orderId = (String) context.get("orderId");
        String orderItemStatusId = (String) context.get("orderItemStatusId");

        try {
            // validate that the order actually exists and get list of non-physical
            GenericValue order = delegator.findByPrimaryKey("OrderHeader", UtilMisc.toMap("orderId", orderId));
            if (UtilValidate.isEmpty(order)) {
                return ServiceUtil.returnError("Order [" + orderId + "] not found");
            }

            // set default item status
            if (UtilValidate.isEmpty(orderItemStatusId)) {
                Debug.logInfo("No status specified when invoicing non-physical items on order [" + orderId + "], using ITEM_PERFORMED", module);
                orderItemStatusId = "ITEM_PERFORMED";
            }

            // get the non-physical items which have been performed
            List<GenericValue> orderItems = order.getRelatedByAnd("OrderItem", UtilMisc.toMap("statusId", orderItemStatusId));
            List<GenericValue> itemsToInvoice = new ArrayList();
            for (GenericValue orderItem:orderItems) {
                if (!UtilOrder.isItemPhysical(orderItem)) {
                    itemsToInvoice.add(orderItem);
                }
            }

            // check if there are items to invoice
            if (UtilValidate.isEmpty(itemsToInvoice)) {
                return UtilMessage.createAndLogServiceError("OpentapsError_PerformedItemsToInvoiceNotFound", locale, module );
            }

            // create a new invoice for the order items
            Map tmpResult = dispatcher.runSync("createInvoiceForOrder", UtilMisc.toMap("orderId", orderId, "billItems", itemsToInvoice, "userLogin", userLogin), 7200, false);  // no new transaction
            if (ServiceUtil.isError(tmpResult)) {
                return tmpResult;
            }

            // change the status of the order items to COMPLETED
            for (GenericValue orderItem:itemsToInvoice) {
                tmpResult = dispatcher.runSync("changeOrderItemStatus", UtilMisc.toMap("orderId", orderItem.getString("orderId"), "orderItemSeqId", orderItem.getString("orderItemSeqId"), "statusId", "ITEM_COMPLETED", "userLogin", userLogin));
            
            // return invoiceId of new invoice created
            String invoiceId = (String) tmpResult.get("invoiceId");

            tmpResult = ServiceUtil.returnSuccess();
            tmpResult.put("invoiceId", invoiceId);
            return tmpResult;
        } catch (GeneralException e) {
            return UtilMessage.createAndLogServiceError(e, module);
        }
    }

So what's there not to love about this code?

  1. It is closely tied to the database. Even though there's not a single line of SQL here, you have to know that orders are stored in "OrderHeader", and that it is related to "OrderItem", and that there are fields like statusId. You also have to use the string literals for status, like ITEM_COMPLETED, ITEM_PERFORMED, etc.
  2. This method depends on things spread out in different parts of the application, like the UtilOrder class and the createInvoiceForOrder and changeOrderItemStatus services.
  3. This code is completely dependent on the ofbiz framework's GenericValue, entity engine delegator, and local dispatcher.
  4. Static Java methods like this, while easier to work with than minilang, do not enjoy the benefits of real object-oriented programming.

In other words, for somebody to write this code, they have to know a lot about the framework, the data model, and the application tier.

Here's a re-write of the everything inside the try ... catch block using the domain driven design:

       // validate that the order actually exists and get list of non-physical
       OrderRepository orderRepository = new OrderRepository(new Infrastructure(dispatcher), userLogin));
       Order order = orderRepository.getOrderById(orderId);
       if (UtilValidate.isEmpty(orderItemStatusId)) {
           Debug.logInfo("No status specified when invoicing non-physical items on order [" + orderId + "], using [" + OrderSpecification.ITEM_STATUS_PERFORMED + "]", module);
           orderItemStatusId = OrderSpecification.ITEM_STATUS_PERFORMED;
       }
       List<GenericValue> itemsToInvoice = order.getNonPhysicalItemsForStatus(orderItemStatusId);

       // check if there are items to invoice
       if (UtilValidate.isEmpty(itemsToInvoice)) {
           return UtilMessage.createAndLogServiceError("OpentapsError_PerformedItemsToInvoiceNotFound", locale, module );
       }

       // create a new invoice for the order items
       Map tmpResult = dispatcher.runSync("createInvoiceForOrder", UtilMisc.toMap("orderId", orderId, "billItems", itemsToInvoice, "userLogin", userLogin), 7200, false);  // no new transaction
       if (ServiceUtil.isError(tmpResult)) {
           return tmpResult;
       }

       // change the status of the order items to COMPLETED
       order.setItemsStatus(itemsToInvoice, OrderSpecification.ITEM_STATUS_COMPLETED);
            
       // return invoiceId of new invoice created
       String invoiceId = (String) tmpResult.get("invoiceId");

       tmpResult = ServiceUtil.returnSuccess();
       tmpResult.put("invoiceId", invoiceId);
       return tmpResult;

This code is the programming equivalent of the missing link: it has many features of the old code, but a few important differences as well. What we have done is push everything related to orders to the Order Entity object, its OrderRepository, and OrderSpecification. We don't care where the order came from, how we can get the items of an order, or even how the status codes of an order are defined any more, because those are all responsibilities of the Order domain objects. (Even the validation that an order was obtained is handled by the OrderRepository, which will throw a RepositoryException if nothing is found from orderId.) We are also no longer tied to the delegator, although the Order domain may itself require the delegator. (The casting of itemsToInvoice to GenericValue is vestigal -- remember that our Entity object extends GenericValue, and a specific Java object may in turn extend Entity.)

We are, however, still tied to the createInvoiceForOrder service and the ofbiz service engine. That will have to wait until the next evolutionary step (which happened the next day). Using the Service class from above, we can implement a POJO version of this service:

public class OrderInvoicingService extends Service {

    private static final String module = OrderInvoicingService.class.getName();

    protected String orderId = null;
    protected String invoiceId = null;
    // by default, non-physical order items in this state will be invoiced
    protected String statusIdForNonPhysicalItemsToInvoice = OrderSpecification.ITEM_STATUS_PERFORMED;

    public OrderInvoicingService(Infrastructure infrastructure, User user, Locale locale) throws ServiceException {
        super(infrastructure, user, locale);
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getInvoiceId() {
        return this.invoiceId;
    }

    /**
     * Set the status id of non-physical order items to be invoiced by invoiceNonPhysicalOrderItems, or
     * OrderSpecification.ITEM_STATUS_PERFORMED will be used
     * @param statusId
     */
    public void setStatusIdForNonPhysicalItemsToInvoice(String statusId) {
        if (statusId != null) {
            statusIdForNonPhysicalItemsToInvoice = statusId;
        }
    }

    public void invoiceNonPhysicalOrderItems() throws ServiceException {
        try {
            // validate that the order actually exists and get list of non-physical
            OrderRepository orderRepository = new OrderRepository(new Infrastructure(dispatcher), user);
            Order order = orderRepository.getOrderById(orderId);
            List<GenericValue> itemsToInvoice = order.getNonPhysicalItemsForStatus(statusIdForNonPhysicalItemsToInvoice);

            // check if there are items to invoice
            if (UtilValidate.isEmpty(itemsToInvoice)) {
                // TODO: Fix localization of errors
                throw new ServiceException("OpentapsError_PerformedItemsToInvoiceNotFound");
            }

            // create a new invoice for the order items
            // because of the way createInvoiceForOrder is written (665 lines of code!) we'd have to do some re-factoring before we can add the items to an existing invoice
            Map tmpResult = getDispatcher().runSync("createInvoiceForOrder", UtilMisc.toMap("orderId", orderId, "billItems", itemsToInvoice, "userLogin", user), 7200, false);  // no new transaction
            if (ServiceUtil.isError(tmpResult)) {
                throw new ServiceException(ServiceUtil.getErrorMessage(tmpResult));
            }

            // change the status of the order items to COMPLETED
            order.setItemsStatus(itemsToInvoice, OrderSpecification.ITEM_STATUS_COMPLETED);

            // set the invoiceId of new invoice created
            this.invoiceId = (String) tmpResult.get("invoiceId");
        } catch (GeneralException ex) {
            throw new ServiceException(ex) ;
        }
    }

}

Then, the original Java static method simply has to pass the parameters to it, execute the method in the OrderInvoicingService, get its result, and pass it back. Here's the content of that try ... catch block again:

            OrderInvoicingService invoicingService = new OrderInvoicingService(new Infrastructure(dispatcher), new User(userLogin), locale);
            invoicingService.setOrderId(orderId);
            invoicingService.setStatusIdForNonPhysicalItemsToInvoice(orderItemStatusId);
            invoicingService.invoiceNonPhysicalOrderItems();

            Map tmpResult = ServiceUtil.returnSuccess();
            tmpResult.put("invoiceId", invoicingService.getInvoiceId());


Congratulations! Now your business logic is a POJO. You can add annotations, use dependency injection with it, and use it with other Java frameworks now. (Is this how that missing link felt, seeing all those primordial forests for the first time?)

Your service is using a legacy ofbiz service "createInvoiceForOrder" still through its getDispatcher() method, but that's not so bad. If you want to use an ofbiz service, using its dispatcher should be part of the game. The real problem, however, is that you still have the XML service definition for the ofbiz service engine plus the stub of Java code, and the ofbiz service engine is still responsible for parameter validation and transaction management. The next step would be to:

  1. Create a new ofbiz service engine called "pojo" to follow the standard convention and execute services which are Java POJOs instead of Java static methods.
  2. Use annotations instead of the services XML to define the services, so that the services can be loaded into the ofbiz service engine without having to create services XML.

Putting It All Together

Now, let's see how we could put all this together to create applications around the domain driven architecture. As we discussed before, related data Entities could be grouped together as an Aggregate, which will have related Factories, Repositories, and Services. For example, an aggregate of concepts related to invoicing might include the Invoice, InvoiceItem, InvoiceStatus, InvoiceContactMech, InvoiceAttribute entities as well as invoice factories, invoice repositories, and several invoicing services: Aggregate.png

Several of these Aggregates may then form a Domain of related business knowledge. For example, the Billing domain may consist of Invoice and Payment aggregates and their related factories, repositories, and services. This Domain would interact with other domains, such as Organization, Ledger, Party, and Product: Domain.png

An application, such as opentaps Financials application, could be built from several relatively independent domains: Application.png

To keep them relatively independent of each other, an interface should be declared for each domain, and they should return interfaces to the repositories, factories, and services. Interfaces are not necessary for the entities, however, since entities represent a data model, which must be implemented in the same way for all opentaps applications. For example, Invoice will always have to have an invoice ID field, and the getInvoiceId() method should always return the value of that field. If different underlying invoicing systems use different types of invoice IDs, it is the responsibility of the invoice repository to parse that and store it in the invoice ID field of Invoice. Invoice need not change.

Each opentaps application would define the actual implementations of all its domains:

// Application will be an abstract super class
public class FinancialsApplication extends Application {

    BillingDomainInterface billingDomain;
    OrganizationDomainInterface organizationDomain;
    // ...
    Map domains();

    public FinancialsApplication() {
        register(BillingDomainInterface, new org.opentaps.financials.domains.billing.BillingDomain());
        register(OrganizationDomainInterface, new org.opentaps.commons.domains.organization.OrganizationDomain());
        // ...
    }

    // the register and get methods will actually be part of the Application super class
    public void register(Class interface, Class domain) {
        this.put(interface, domain);
    }

    public Domain get(Class interface) {
        // check that the domain is registered first and throw an exception if it is not
        return this.get(interface).newInstance();
    }

Then, when another domain is required , it can be retrieved by traversing through the application:

    //  inside a service, repository, or factory
    BillingDomainInterface billingDomain = (BillingDomainInterface) this.getDomain().getApplication().getDomain(BillingDomainInterface);
    InvoiceRepositoryInterface invoiceRepository = billingDomain.getInvoiceRepository();