4. [TD]: Layered Architectures
Keywords: multi-layer architecture, Spring, dependency injection.
4.1. Introduction
Let’s review what we’ve done so far:
- In Part 1 of the exercise ELECTIONS, no classes were used. We built a solution as we would have built it in the C language.
- In Part 2 of the exercise, two classes were introduced:
- [ListeElectorale], which represents the attributes (id, name, votes, seats, eliminated) of a candidate list
- [ElectionsException], a class for unhandled exceptions. This type of exception is used whenever a fatal error occurs in the election application. It is unhandled, c.a.d, meaning the developer is not required to handle it with a try-catch block.
Until now, the calculation of election results has been handled by a method [main] of a class [MainElections]
The previous solution includes three standard phases:
- data acquisition, lines 17-18
- calculating the solution, lines 19-20
- displaying and/or saving the results, lines 21–22
Only phase 2 is truly constant. Phase 1 can vary: data can come from the keyboard as in the examples studied, from a text file, from a graphical interface, from a database, from the network, ... Similarly, there are multiple ways to present the results in phase 3: displaying them on the screen as done in the examples studied, saving them to a file, to a database, sending them over the network, etc.
More generally, an application can often be modeled as three layers, each with a well-defined role:
![]() |
This architecture is also called "three-tier architecture." The term "three-tier" normally refers to an architecture where each tier is on a different machine. When the tiers are on the same machine, the architecture becomes a "three-layer" architecture.
- The [metier] layer contains the application’s business rules. For our election application, these are the rules that calculate the seats won by the various lists once the votes obtained by each are known. This layer requires data to function. For example, in the election application:
- the lists, each with its name and number of votes
- the number of seats to be filled
- the electoral threshold below which a list is eliminated
In the diagram above, the data can come from two sources:
- the data access layer or [dao] (DAO = Data Access Object) for data already stored in files or databases. This could be the case here for the names of the lists, the number of seats to be filled, and the electoral threshold. Indeed, this information is known before the election itself.
- the user interface layer or [ui] (UI = User Interface) for data entered by the user or displayed to the user. This could be the case here with the votes for the lists, which are not known until the last moment, as well as with the display of the election results.
- In general, the [dao] layer handles access to persistent data (files, databases) or non-persistent data (network, sensors, etc.).
- The [ui] layer, on the other hand, handles interactions with the user, if there is one.
- The three layers are made independent through the use of Java interfaces.
- There are various methods for integrating these layers into the application. We will use a tool called "Spring." In the diagram, it cuts across the other layers.
We will revisit the previously developed [Elections] application to give it a three-tier architecture. To do this, we will examine the [ui, metier, dao] layers one by one, starting with the [dao] layer, which handles persistent data.
First, we need to define the interfaces for the different layers of the [Elections] application.
4.2. The interfaces of the [Elections] application
Remember that an interface defines a set of method signatures. The classes that implement the interface provide the implementation for these methods.
Let’s return to the 3-tier architecture of our application:
![]() |
In this type of architecture, it is often the user who takes the initiative. The user makes a request in [1] and receives a response in [8]. This is called the request-response cycle. Let’s take the example of calculating the number of seats won on election night. This will require several steps:
- The [ui] layer will need to ask the user for the number of votes received by each list. To do this, it will need to present the user with the names of the competing lists. The user will then simply enter the number of votes next to each list and request the seat calculation.
- The [ui] layer does not have the names of the lists. These are stored in the data source to the right of the diagram. It will use the path [2, 3, 4, 5, 6, 7] to retrieve them. Operation [2] is the request for the lists, and operation [7] is the response to that request. Once this is done, it can present them to the user via [8].
- The user will transmit the number of votes obtained by each list to the [ui] layer. This is the [1] operation described above. During this step, the user interacts only with the [ui] layer. This layer will, in particular, verify the validity of the entered data. Once this is done, the user will request the list of seats obtained by each of the lists.
- The [ui] layer will ask the business layer to calculate the seats. To do this, it will send the data it received from the user to the business layer. This is operation [2].
- The [metier] layer needs certain information to complete its task. It already has the lists from operation (b). It also needs the number of seats to be filled and the electoral threshold value. It will request this information from layer [dao] via the path [3, 4, 5, 6]. [3] is the initial request, and [6] is the response to that request.
- Having all the data it needed, layer [metier] calculates the seats won by each of the lists.
- The [metier] layer can now respond to the request made by the [ui] layer in (d). This is the [7] path.
- Layer [ui] will format these results to present them to the user in an appropriate form and then display them. This is the path [8].
- One can imagine that these results need to be stored in a file or a database. This can be done automatically. In this case, after operation (f), the [metier] layer will instruct the [dao] layer to save the results. This will be the path [3, 4, 5, 6]. This can also be done only upon user request. The path [1-8] will be used by the request-response cycle.
We can see from this description that a layer uses the resources of the layer to its right, never those of the layer to its left. Consider two contiguous layers:
![]() |
The [A] layer makes requests to the [B] layer. In the simplest cases, a layer is implemented by a single class. An application evolves over time. Thus, the [B] layer may have different implementation classes, such as [B1, B2, ...]. If the [B] layer is the [dao] layer, the latter may have an initial implementation, [B1], that retrieves data from a file. A few years later, we may want to store the data in a database. We will then build a second implementation class, [B2]. If, in the initial application, the [A] layer worked directly with the [B1] class, we would be forced to partially rewrite the code of the [A] layer. Suppose, for example, that we wrote something like the following in the [A] layer:
- line 1: an instance of the [B1] class is created
- line 3: data is requested from this instance
If we assume that the new implementation class [B2] uses methods with the same signature as those of the class [B1], we will need to change all instances of [B1] to [B2]. This is a very favorable scenario and quite unlikely if you haven’t paid attention to these method signatures. In practice, it is common for the classes [B1] and [B2] to have different method signatures, meaning that a significant portion of the [A] layer must be completely rewritten.
We can improve this by placing an interface between the [A] and [B] layers. This means that the method signatures presented by the [B] layer to the [A] layer are fixed in an interface. The previous diagram then becomes the following:
![]() |
The [A] layer no longer communicates directly with the [B] layer but with its [IB] interface. Thus, in the code of the [A] layer, the implementation class [Bi] of the [B] layer appears only once, when implementing the [IB] interface. Once this is done, it is the interface [IB] and not its implementation class that is used in the code. The previous code becomes the following:
- line 1: a [ib] instance implementing the [IB] interface is created by instantiating the [B1] class
- line 3: data is requested from the [ib] instance
Now, if we replace the [B1] implementation of the [B] layer with a [B2] implementation, and both of these implementations adhere to the same [IB] interface, then only line 1 of the [A] layer needs to be modified, and no others. This is a major advantage that alone justifies the systematic use of interfaces between two layers.
We can go even further and make the [A] layer completely independent of the [B] layer. In the code above, line 1 is problematic because it hard-codes a reference to the [B1] class. Ideally, the [A] layer should be able to use an implementation of the [IB] interface without having to name a class. This would be consistent with our diagram above. We can see that the [A] layer interfaces with the [IB] interface, and there is no reason why it would need to know the name of the class that implements this interface. This detail is not useful to the [A] layer.
The Spring framework (http://www.springframework.org) enables this result. The previous architecture evolves as follows:
![]() |
The cross-cutting layer [Spring] will allow a layer to obtain, via configuration, a reference to the layer located to its right without having to know the name of the layer’s implementation class. This name will be in the configuration files and not in the Java code. The Java code for the [A] layer then takes the following form:
- line 1: a [ib] instance implementing the [IB] interface of the [B] layer. This instance is created by Spring based on information found in a configuration file. Spring will handle creating:
- the [b] instance implementing the [B] layer
- the instance [a] implementing the layer [A]. This instance will be initialized. The [ib] field above will be assigned the reference [b] of the object implementing the [B] layer
- Line 3: Data is requested from the [ib] instance
We can now see that the implementation class [B1] of layer B does not appear anywhere in the code of layer [A]. When the implementation [B1] is replaced by a new implementation [B2], nothing will change in the code of the class [A]. We will simply change the Spring configuration files to instantiate [B2] instead of [B1].
The combination of Spring and Java interfaces brings a decisive improvement to application maintenance by making the layers of the application tightly coupled with one another. This is the solution we will use for the [Elections] application.
Let’s return to the three-tier architecture of our application:
![]() |
In simple cases, we can start from the [metier] layer to discover the application’s interfaces. To function, it needs data:
- already available in files, databases, or via the network. This data is provided by the [dao] layer.
- not yet available. It is then provided by the [ui] layer, which obtains it from the application user.
What interface must the [dao] layer provide to the [metier] layer? What interactions are possible between these two layers? The [dao] layer must provide the following data to the [metier] layer:
- the number of seats to be filled
- the electoral threshold below which a list is eliminated
- the names of the lists
This information is known before the election and can therefore be stored. In the direction [metier] -> [dao], the [metier] layer can request that the [dao] layer record the election results, specifically the seats won by the various lists.
With this information, we could attempt an initial definition of the interface for the [dao] layer:
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
- Line 1: The interface is named [IElectionsDao]. It defines four methods:
- three methods for reading data from the data source: [getSeuilElectoral, getNbSiegesAPourvoir, getListesElectorales]. These three methods will allow the [metier] layer to obtain the data characterizing the current election.
- one method for writing data to the data source: [setListesElectorales]. This method will allow the [metier] layer to request the recording of the results it has calculated.
Let’s return to the three-layer architecture of our application:
![]() |
What interface should the [metier] layer present to the [ui] layer? Let’s examine the possible interactions between these two layers.
- The [ui] layer will be responsible for asking the user for votes for the various competing lists. To do this, it must know the number of lists. It can request this information from the [metier] layer, which can in turn request the table of competing lists from the [dao] layer. If the [metier] layer has this table, it might as well transfer it to the [ui] layer. This layer will then have the names of the lists and can refine its messages to the user by asking, for example, "Number of votes for List A."
- Once layer [ui] has obtained the votes for all lists, it will request the seat calculation from layer [metier]. This layer will be able to perform the calculation and return the result to layer [ui].
- Layer [ui] can then present these results to the user. The user may also request that they be saved.
- The [ui] layer may also wish to present additional information to the user, such as the electoral threshold or the number of seats to be filled.
With this information, we could attempt an initial definition of the interface for layer [metier] :
public interface IElectionsMetier {
public ListeElectorale[] getListesElectorales();
public int getNbSiegesAPourvoir();
public double getSeuilElectoral();
public void recordResultats(ListeElectorale[] listesElectorales);
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
}
- line 1: the interface is called [IElectionsMetier]. It defines the following methods:
- line 3: a method [getListesElectorales] that will allow the [ui] layer to obtain the array of competing lists;
- line 5: the [getNbSiegesAPourvoir] method retrieves the number of seats to be filled;
- line 7: the method [getSeuilElectoral] retrieves the electoral threshold;
- line 11: a method [calculerSieges] (line 36) that will allow the layer [ui] to request the calculation of seats once the vote counts for the various lists are known. The parameter is the array of competing lists, without their seats and without the eliminated boolean. The returned result is this same array, this time with the [sièges, elimine] fields initialized;
- line 9: a method [recordResultats] that will allow the [ui] layer to request the recording of results.
Note: Due to its position, the [métier] layer reuses some of the methods from the [DAO] layer to make them available to the [UI] layer. Because of this redundancy, one might be tempted to consolidate everything into a single layer that would combine both the business logic and data access. This single layer is sometimes called the model, the M in the acronym MVC (Model-View-Controller). MVC is a design pattern commonly used in web applications.
Let’s examine the signature of the [calculerSieges] method:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
It was stated earlier: “The parameter is the array of competing lists, without their seats and without the eliminated boolean. The result is the same array, this time with the [sièges, elimine] fields.” The method signature could also be as follows:
public void calculerSieges(ListeElectorale[] listesElectorales);
The parameter [listesElectorales] is an object reference, in this case an array. Each element is in turn an object reference, in this case of type [ListeElectorale]. The method [calculerSieges] will modify the fields [sieges, elimine] of each of these objects. The calling method holds a pointer [listesElectorales] that:
- Before the call, this is a reference to an object array [ListeElectorale] whose fields [sieges, elimine] are uninitialized;
- after the call, is the reference (the same one) to an array of [ListeElectorale] objects with its [sieges, elimine] fields initialized;
So why use the signature:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
When writing an interface, it is important to remember that it can be used in two different contexts: local and remote . In the local context, the calling method and the called method are executed in the same JVM (Java Virtual Machine):
![]() |
If the [ui] layer calls the calculerSieges method of the [DAO] layer, it does indeed have a reference to the [ListeElectorale[] listesElectorales] that it passes to the method.
In the remote context, the calling method and the called method are executed in different JVM layers:
![]() |
Above, layer [ui] runs in JVM 1 and layer [métier] in JVM 2 on two different machines. The two layers do not communicate directly. Between them is an intermediate layer that we will call the [1] communication layer. This layer consists of a transmission layer [2] and a reception layer [3]. The developer generally does not have to write these communication layers. They are generated automatically by software tools. The [metier] layer is written as if it were running in the same JVM as the [DAO] layer. Therefore, there is no code modification.
The communication mechanism between the [ui] layer and the [métier] layer is as follows:
- The [ui] layer calls the calculerSieges method of the [métier] layer, passing it the parameter [ListeElectorale[] listesElectorales1];
- this parameter is actually passed to the transmission layer [2]. This layer will transmit the value of the parameter listesElectorales1 over the network, not its reference. The exact form of this value depends on the communication protocol used;
- the receiving layer [3] will retrieve this value and use it to reconstruct an object [ListeElectorale[] listesElectorales2] that mirrors the initial parameter sent by the [metier] layer. We now have two identical objects (in terms of content) in two different JVM layers: listesElectorales1 and listesElectorales2.
- The receiving layer will pass the listesElectorales2 object to the calculerSieges method of the [métier] layer, which will persist it in the database. After this operation, the reference listesElectorales2 points to an array of [ListeElectorale] objects with their [sieges, elimine] fields initialized. This is not the case for the object listesElectorales1, to which the layer [ui] has a reference. If we want the layer [ui] to have a reference to the object listesElectorales2, we must pass it to the layer. Therefore, we use the following signature for the method [calculerSieges]:
public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);
- With this signature, the calculerSieges method will return the reference listesElectorales2 as a result. This result is returned to the receiving layer [3], which had called the layer [métier]. The latter will return the value (not the reference) of listesElectorales2 to the sending layer [2];
- the emitting layer [2] will retrieve this value and use it to reconstruct an object [ListeElectorale[] listesElectorales3] image of the result rendered by the calculerSieges method of the [métier] layer.
- The object [ListeElectorale[] listesElectorales3] is passed to the method of the [ui] layer, whose call to the calculerSieges method of the [DAO] layer had initiated this entire mechanism;
In this process, objects of type [ListeElectorale] will pass between the [2] and [3] layers:
- when the [2] layer transmits the value of a [ListeElectorale] object to the [3] layer, the object is said to be serialized. The exact form of this serialization depends on the communication protocol used;
- When the [3] layer retrieves the value of a [ListeElectorale] object in order to create a new [ListeElectorale] object, the object is said to be deserialized;
In order for an object to undergo this serialization/deserialization, certain protocols require that the object implement the [Serializable] interface. This interface is merely a marker; there are no methods to implement. Therefore, the class [ListeElectorale] will now be declared as follows:
public abstract class ListeElectorale implements Serializable {
private static final long serialVersionUID = 1L;
- The field on line 2 is required. It can be kept as is and used for any class of type [Serializable].
4.3. The exception class
Let’s return to the interface of the [DAO] layer:
![]() |
public interface IElectionsDao {
public double getSeuilElectoral();
public int getNbSiegesAPourvoir();
public ListeElectorale[] getListesElectorales();
public void setListesElectorales(ListeElectorale[] listesElectorales);
}
These methods work with a database and may encounter various errors, such as a SGBD not available. When writing a method, you must always anticipate error cases. These are typically signaled by an exception. We have already encountered the [ElectionsException] class in Section 3.3. We will continue to use it but enhance it as follows:
package ...;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// exception class for the Elections application
// the exception is uncontrolled
public class ElectionsException extends RuntimeException implements Serializable {
// serial ID
private static final long serialVersionUID = 1L;
// local fields
private int code;
private List<String> erreurs;
// manufacturers
public ElectionsException() {
super();
}
public ElectionsException(int code, Throwable e) {
// parent
super(e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message, Throwable e) {
// parent
super(message,e);
// local
this.code = code;
this.erreurs = getErreursForException(e);
}
public ElectionsException(int code, String message) {
// parent
super(message);
// local
this.code = code;
List<String> erreurs = new ArrayList<>();
erreurs.add(message);
this.erreurs = erreurs;
}
public ElectionsException(int code, List<String> erreurs) {
// parent
super();
// local
this.code = code;
this.erreurs = erreurs;
}
// list of exception error messages
private List<String> getErreursForException(Throwable th) {
// retrieve the list of exception error messages
Throwable cause = th;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
// getters and setters
...
}
- lines 16-17: the type [ElectionsException] encapsulates:
- an error code, line 16;
- a list of error messages, line 17;
The class supports five constructors:
- line 20: ElectionsException()
- Line 24: ElectionsException(int code, Throwable e): The second parameter is of type [Throwable], which is the superclass of the [Exception] class. This constructor allows you to wrap the exception e with an error code. The [Throwable] type (and therefore the Exception type) allows you to wrap one or more exceptions. The idea is:
- to catch an exception that occurs;
- enrich it with a message by encapsulating it in a new exception;
- to throw the new exception;
Encapsulation occurs on line 34 via the [super(message,e)] statement. This encapsulation process can be repeated, and the initial exception can be enriched with various messages. This is referred to as an exception stack. The [private List<String> getErreursForException(Throwable th)] method allows you to retrieve the various messages associated with the encapsulated exceptions:
- (continued)
- (continued)
- The encapsulated exception is obtained using the Throwable method [Throwable].getCause();
- the message associated with an exception is the String [Throwable].getMessage() method;
- (continued)
- lines 28-29: the [code, erreurs] fields are constructed;
- line 32: public ElectionsException(int code, String message, Throwable e): this constructor is similar to the previous one, except that it enriches the exception it will encapsulate with both a code and a message;
- line 40: public ElectionsException(int code, String message): constructor without exception encapsulation;
- line 50: public ElectionsException(int code, List<String> errors): constructor without exception encapsulation or message;
The [ElectionsException] class can be used as follows:
where the message may or may not be present. Once created, the [ElectionsException] exception is not intended to encapsulate new exceptions. In the example above, it encapsulates the e1 exception and the exceptions that e1 encapsulates. There are no further encapsulations beyond that.
The [ElectionsException] class can also be used as follows:






