When building a large-scale application, one has to carefully design the code structure in the project. A poor structure can lead to maintainability issues and decrease the productivity of the development team. When the system grows, it will be hard to navigate through the structure and locate the class implementing a piece of functionality.
In this article, we describe the structure that worked well for us at Sparkbit in an enterprise system built using Angular 2.
You may call us old-fashioned, but we follow the MVC design pattern. Thus, in our frontend code we identify three main types of entities:
All our business logic is implemented within the services. This includes client-side logic as well as the communication with backend endpoints. Model objects define elements of our domain. They just carry the data. In a separate post we’ll show how do we transform responses from external systems into model objects with a full type validation. Finally, the components implement the view. We hope there is nothing controversial about this part.
Structuring of the Components is most challenging. The tutorial-style structure with one, flat directory does not scale too well and a strategy for nesting packages is necessary.
In the post about component-based architecture, we’ve shown that an Angular 2 application is a tree of components. It might be tempting to mimic this tree in the structure also in the sources. This approach works well in relatively small applications, where each component is usually used just in one context. As the package structure would reflect the DOM, it would be also quite easy to navigate through the sources.
However, this strategy does not work in cases, when we reuse the same component in multiple contexts. We would end up importing components from unrelated parts of the application, breaking loose coupling rules.
The DOM-like structure seemed natural for an Angular 2 application, but as it does not fit into more complex systems, we need to look for something else. There are patterns well established in backend development – the package structure should reflect the functionality implemented by particular components, not the place in the interface, where they are used. This approach has some clear advantages:
- The references between components are much clearer than in the view-oriented approach. Each component import only elements from packages that implement the functionality needed in this component. The dependency graph is much better directed and it is easier to spot encapsulation violations.
- As the structure is decoupled from the usage of the components, it supports building libraries, not only applications (see the section about multi-module systems).
Unfortunately, life is not that simple. In an Angular 2 application we usually have components that implement some business functionality, but we also have to do the layouting. For example, a whole page view is a component as well (as everything is a component). It usually defines only what should be displayed and supervises the interactions between the components. It often uses a router as well. By definition it is very tightly coupled to the place in the application, where it is displayed (and thus it is not a good candidate for re-use). We will discuss this in more detail in an article about router and reusability. This type of components does not fit well into the semantic structure.
As we have observed, neither the view-oriented nor the semantic structure work really well for Angular 2 components. So, we combine these two: “pure” components form a structure using the semantic strategy, while “layouting” components create a separate hierarchy using the view-oriented approach.
There are two competing styles of structuring tests in an application. Some people prefer to put tests together with the tested component. This approach is in line with the encapsulation principles. The second option is to keep the tests in a separate hierarchy. In this case it is easier to distinguish between application and test code and it is easier to manage the build process. This approach is also more natural for backend developers. While we think that both styles work, in our projects we use the second one.
Multi-module Systems in Angular 2
In an enterprise environment it seldom happens that an application lives in isolation. It is usually part of a larger portfolio of systems that need to work together and that should provide a unified user experience. The systems often have non-empty functional overlap. Having shared backend libraries is a standard in almost every enterprise. The same good practice should be applied in the frontend.
The following diagram shows the module-level uses view of our system:
core module implements shared services and reusable APIs. The
component library module provides components that implement business functionality needed in multiple systems. Finally, the
application module implements the application-specific business logic, the application-specific domain and all the layouting.
Each of the modules is delivered as a separate npm package. This way, multiple systems from the portfolio may use (and contribute to) the same core and component library.
We have discussed a way to structure large projects in Angular 2, both on a system and module levels. We believe that such a structure simplifies code maintenance and makes it possible to reuse components across multiple applications. It works fine for us and we believe you will find it useful too.