29. Application exercise: version 11
29.1. Introduction
In previous versions of the client/server tax calculation application, the [business logic] layer that implements the business rules for this calculation was on the server side. We now propose to move it to the client side. What is the benefit? Some of the work previously done by the server will be moved to the client side. Consider a scenario where a server is queried by N clients; N tax business calculations will be performed by the clients. In previous versions, the server performed these N business calculations. Because it no longer performs the business calculation, the server will respond more quickly to its clients and will therefore be able to serve more of them simultaneously.
The client/server architecture becomes as follows:

- the [business] layer [10] has been duplicated [12] on the client;
- a new script [main2] [11] has been added to the client;
The web client will have two ways to calculate the tax for the list of taxpayers found in [3]:
- use the method from the previous version. It uses the server’s [business] layer [10]. The [main] script will use this method;
- simply request the tax authority data from the server [2-4] and then use the client-side [business] layer [12];
We will compare the performance of the two methods.
29.2. The web server
The web server directory structure will be as follows:

- The [http-servers/06] directory is initially created by copying the [http-servers/05] directory. We will indeed retain the features of the previous version 10. We will simply add a new feature to it. This is implemented by the presence of a new controller [get_admindata_controller] [1]. The other controller [calculate_tax_controller] is none other than the old [index_controller] that has been renamed;
29.3. Configuration
The server will offer two service URLs:
- [/calculate-tax] to calculate the tax for a list of taxpayers passed in the body of a POST request. It therefore corresponds to the [/] URL from the previous version 10;
- [/get-admindata] returns the JSON string of tax administration data;
The configuration [config] associates each of these URLs with the controller that handles it:
29.4. The main script [main]
The main script [main] restructures the [main] script from the previous version:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
- lines 88–93: the [calculate_tax] function handles the URL [/calculate-tax];
- lines 95–100: the [get_admindata] function handles the URL [/get-admindata];
- These two functions do nothing on their own. They immediately hand control over to the main controller [main_controller] in lines 37–86;
- lines 37–86: the main controller [main_controller] is nothing more than the [index] function from the previous version, with one minor difference: whereas the [index] function handled only a single URL, here [main_controller] handles two URLs. It must therefore have these processed by one of the two controllers [calculate_tax_controller, get_admin_data_controller];
- Lines 39–40: We retrieve the requested action [calculate_tax] or [get_admindata]. This information is in the URL path [request.path]. Depending on the case, [request.path] is either [/get-admindata] or [/calculate_tax]. The split on line 40 will yield two elements:
- the empty string for the part preceding the /;
- the name of the requested action for the part following the /;
- lines 62-63: once the URL action has been retrieved, we know which controller to use to handle the URL. This information is in the configuration [config];
29.5. Controllers
The [calculate_tax_controller] is none other than the [index_controller] from the previous version.
The [get_admindata_controller] controller is as follows:
- The URL [/get-admindata] must return the JSON string of the tax administration data;
- line 6: this data was retrieved by the main script [main] and placed in the dictionary [config] as an [AdminData] object. We return the dictionary of this object;
29.6. Postman Tests
We start the web server, the DBMS, and the mail server [hMailServer]. Then, using a Postman client, we calculate the tax for several taxpayers:

In the Postman console, the client/server dialogue is as follows:
POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
…
{
"marié": "oui",
"enfants": 3,
"salaire": 200000
}
]
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:02:07 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347…]}}
Now let’s request the URL [/get-admindata] with a GET request:

The client/server dialogue in the Postman console is as follows:
GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT
{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
29.7. The web client


The [http-clients/06] folder is initially created by copying the [http-clients/05] folder. The modification work essentially consists of:
- modifying the [config_layers] configuration so that it now includes a [business] layer. Previously, it only had a [DAO] layer;
- adding a new method to the [dao] layer;
- writing a script [main2] that will rely on the client’s [business] layer to calculate taxpayers’ taxes;
29.7.1. Client Layer Configuration
Layer configuration occurs in two places:
- in the [config] configuration, which must include the folder containing the [business] layer implementation in the client’s dependencies. This folder was already included in the dependencies:
absolute_dependencies = [
# dossiers du projet
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Constantes, tranches
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# scripts de configuration
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
Then the [config_layers] file must be modified:
- lines 4–6: instantiation of the [business] layer;
- lines 13-16: the [business] layer is returned in the layer dictionary;
29.7.2. Implementation of the [dao] layer

The [dao] layer will implement the following [InterfaceImpôtsDaoWithHttpClient] interface:
- line 5: the interface [InterfaceImpôtsDaoWithHttpClient] inherits from the abstract class [AbstractImpôtsDao], which manages access to the client’s file system. Note that it has an abstract method [get_admindata];
- lines 7–10: the method [calculate_tax_in_bulk_mode] that we defined in the previous version allows for the calculation of tax for a list of taxpayers;
This interface is implemented by the following [ImpôtsDaoWithHttpClient] class:
- line 13: the [TaxDaoWithHttpClient] class implements the [TaxDaoWithHttpClientInterface] interface. It therefore derives from the [AbstractTaxDao] class;
- lines 65–66: the [calculate_tax_in_bulk_mode] method discussed in the previous version;
- lines 29–62: the [get_admindata] method, which the parent class [AbstractImpôtsDao] has declared as abstract. It is therefore implemented in the child class;
- lines 33–35: the URL of the web service that the [get-admindata] method must query is determined. These service URLs are defined in the client’s [config] configuration:
# le serveur de calcul de l'impôt
"server": {
"urlServer": "http://127.0.0.1:5000",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata"
}
},
- (continued)
- lines 9–12: the two web server URLs;
- lines 37–44: the service URL is queried synchronously;
- lines 46–42: if the configuration requires it, the server’s response is logged;
- line 57: we know that the server sent a JSON string of a dictionary;
- lines 58–60: if the HTTP status of the response is not 200, then an exception is thrown;
- lines 61-62: the [AdminData] object encapsulating the tax administration data sent by the server is returned;
29.8. The [main, main2] scripts
The [main] script is the one from the previous version. It uses the [calculate_tax_in_bulk_mode] method from the [dao] layer and therefore uses the server’s [business] layer;
The [main2] script does the same thing as the [main] script but uses the client’s [business] layer:
- lines 26-27: retrieve data from the tax authority’s server;
- lines 28-31: then the taxpayers' tax is calculated locally;
29.9. Client tests
In each of the scripts [main, main2], we log the start and end of the script. This allows us to calculate the script’s execution time. Let’s make some predictions:
- the [main] script from the previous version:
- creates N threads that run simultaneously;
- each thread processes a batch of taxpayers for whom it calculates the tax via a single request to the server;
- because the N threads run simultaneously, the N+1 request is sent before the N request has received its response. Thus, the N requests cost more than a single request but probably not much more. There are also 11 (the number of taxpayers) business calculations on the server;
- the [main2] script in this version:
- makes a single request to the server;
- performs 11 business calculations locally on the client;
The business calculations will take the same amount of time whether performed on the server or the client. The difference will therefore lie in the requests. We can therefore expect the execution time of [main] to be slightly longer than that of [main2].
We launch the version 11 server, the DBMS, and the [hMailServer] mail server. On the server side, we set the [sleep_time] parameter to zero so that both tests are executed under the same conditions.
Execution 1 [main]
The execution of [main] produces the following logs:
2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables
The execution time was [051214-016079] nanoseconds (line 17 – line 1), i.e., 35 milliseconds and 135 nanoseconds.
We can see that between the first request made to the server and the last response received by the client, the duration is the same [051214-016079] (line 15 – line 1), 35 milliseconds and 135 nanoseconds.
Execution 2 [main2]
The execution of [main2] yields the following logs:
2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables
The execution time was [349975-303520] nanoseconds (line 3 - line 1), i.e., 46 milliseconds and 455 nanoseconds. Quite unexpectedly, [main] is faster than [main2].
We see that the single request from [main2] took [345084-303520] (line 2 – line 1), i.e., 41 milliseconds and 564 nanoseconds. The tax calculation then took [349975-345084] (line 3 – line 2), i.e., 4 milliseconds and 91 nanoseconds. It is the HTTP request that accounts for the execution time. Surprisingly, we see here that the single request from [main2] took longer [41 milliseconds] than the four simultaneous requests from [main] [35 milliseconds].
On the server side, the logs are as follows:
2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
- line 5: the first request from the client [main];
- line 14: the last response to the client [main]. There are 6 milliseconds and 647 nanoseconds between the two;
- lines 15–16: the single request from client [main2]. The response is instantaneous;