For future developers, this document will serve as a guide to understanding the backend and how to work with it, as well as the numerous design choices made.
Sothea-Backend provides a REST API for Sothea-Frontend to interact with, to do CRUD operations on patients.
It is written in Go, due to its speed, reliability and ease of use. The backend uses a PostgreSQL database to store
patient data.
It also draws inspiration
from Bob's Clean Architecture.
Some key concepts used include Dependency Rules and use cases, which make testing and maintenance easier.
For basic deployment instructions, refer to the README.md file.
The center of the backend is the patients entity, a representation of a patient's data. A patient comprises the
following categories, each with their own fields:
Patient SQL schema: /sql/patients_setup.sql
Patient Golang struct schema: /entities
- Admin
- Past Medical History
- Social History
- Vital Statistics
- Height and Weight
- Visual Acuity
- Fall Risk
- Doctor's Consultation
Patients go through the physical health screening stations with the admin station first, and the rest having no
guaranteed order.
Hence, if a patient exists, they will have an admin row, but may not have the other categories present yet.
Additionally, patients may have multiple visits, with multiple rows for each category, representing the previous years
of visits.
Every row in the patient database will have the following structure:
+------------+----------+-----------------------+
| patient_id | visit_id | rest of categories |
+------------+----------+-----------------------+
The patient id is used to uniquely identify a patient, while the visit id is used to narrow down which visit the visit is associated with. The choice to use an visit id to identify the visit an visit is associated with, instead of the date was made to allow for easier querying, since the Date data type might be tricker to work with.
To view the details of the types used in the backend, refer to this google sheet.
The database used is PostgreSQL. To interact with the db, the Golang database driver used is lib/pq, with raw SQL statements. We have decided against using an ORM due to the interesting nature of the patient entity, which may or may not have all categories filled out, save for the admin category. Safer alternatives such as sqlx or sqlcl could be considered in the future, but we decided to stick with raw SQL for now.
Additionally, we've tried to keep the schema simple, choosing not to compute derived fields at the database or backend level.
.
├── README.md - Contains basic instructions for setting up the backend.
├── DOCS.md - Hello future developer! This is the backend documentation file.
├── go.mod
├── go.sum
├── .gitignore - Contains files to be ignored by git.
├── config.json - Contains configuration for backend in development mode.
├── prod.json - Contains configuration for backend in production mode.
├── Dockerfile - Contains the Dockerfile for the database.
├── main.go - Entry point for the backend.
├── .github - Folder containing github actions.
├── controllers
│ ├── middleware
│ │ └── auth.go - Middleware for authentication.
│ ├── patient_handler.go - Handles patient requests.
│ └── login_handler.go - Handles login requests.
├── entities - Contains struct definitions for the patient entity and errors.
│ ├── admin.go
│ ├── pastmedicalhistory.go
│ ├── socialhistory.go
│ ├── vitalstatistics.go
│ ├── heightandweight.go
│ ├── visualacuity.go
│ ├── doctorsconsultation.go
│ ├── patient.go - Contains the patient struct definition encompassing all categories, as well as PatientUseCase and PatientRepository interfaces.
│ ├── patientmeta.go - Contains the patientmeta struct definition.
│ ├── patientvisitmeta.go - Contains the patientvisitmeta struct definition.
│ ├── user.go - Contains the user struct definition.
│ ├── errors.go - Contains custom error definitions.
│ └── helper.go - Contains helper functions for the entities.
├── mocks - Folder containing autogenerated mocks and dummy patient data for testing.
├── repository
│ ├── postgres
│ │ └── postgres_patient.go - Contains the postgres implementation of the PatientRepository interface.
│ └── tmp - Contains output.csv files, a temporary file generated when exporting patient data.
├── sql
│ └── patients_setup.sql - Contains the SQL schema for the patients table.
├── usecases
│ ├── patient_ucase.go - Contains the PatientUseCase interface and its implementation.
│ └── login_ucase.go - Contains the LoginUseCase interface and its implementation.
├── util
│ └── helper.go - Contains general helper functions for the backend.
Test files have been excluded from the directory structure for brevity.
We have defined some custom errors in entities/errors.go
.
They serve to make passing errors around easier, and to provide more context to the error, such as whether a Patient or
PatientVisit was not found.
HTTP Error Codes Used:
- 400: Bad Request
- 401: Unauthorized
- 404: Not Found
- 500: Internal Server Error
The backend uses a simple authentication middleware to check if the user is authenticated in the
controllers/middleware/auth.go
file.
The middleware checks for the presence of a JWT token in the Authorization header, and verifies it using the secret key
in the config file.
The backend uses a config.json
or prod.json
file to store configuration settings, and extracts the values on startup
using Viper.
The configuration file helps to keep sensitive information such as the database URL and JWT secret key secure.
The backend uses the standard Go testing package for testing. Testing is done at the controller and repository levels, with the use of mocks to simulate the usecases.
For testing at the controller level, we used vektra/mockery to generate mocks for
the usecases and repositories.
The mocks are stored in the mocks
folder, and are used in the controller tests.
For testing at the repository level, we opted for using Dockertest to spin up a
temporary PostgreSQL container for testing, and to run the tests against it.
This is to ensure that the data access layer, which is far more complex to mock, performs exactly as expected.
See why you probably shouldn't mock the
database here.
SQL Fields: snake_case
e.g. consultation_notes
Golang Struct Fields: CamelCase with first letter capitalised
e.g. RegDate
JSON Fields: camelCase
e.g. pastSmokingHistory
We have chosen to use pointers for fields that can be null in the database, such as dob
, age
, sent_to_id
, photo
,
etc.
While null types exist in the lib/pq library, we have chosen to use pointers instead.
For example, instead of using sql.NullString
, we use *string
for fields that can be null.
This works at the data access layer, but fails at the controller layer where JSON marshalling happens.
For primitive types like bool, they can only take on 2 values: true, false. When I marshal JSON into a struct, null
fields get converted into the false value for booleans. For binding: required
gotags, this gets treated as the field
being null.
For both optional and not null fields, this means the validator cannot tell if a value is false because it is explicitly
assigned to be false, or because it was null, and got marshalled into the false value.
The only workaround is to use a pointer, which allows it to take on a third value, nil.
Additionally, I updated String() methods to use SafeDeref, a helper method, to dereference pointers to their respective null types if needed instead of just nil, which cannot be printed.
An important feature of the backend is for users to be able to easily export the patient data to a CSV file.
Due to the lack of support for CSV exports in the lib/pq database drivers, we had to implement the feature manually.
One of the approaches used was to execute the
We have since moved to using a simple existing golang library, sqltocsv, which
writes the rows to a csv file easily.COPY
command in the PostgresQL server to generate a csv file,
leveraging the in-built feature. However, due to storage being isolated in the Docker container, an additional volume
mount is needed to access the generated file.
While it is a little messy, this method doesn't require us to use an external client such as psql, and doesn't us to
handle the messy typecasting of data to strings.
This feature allows importing existing CSV files to the database. The CSV file must be in the correct format, with the
correct headers, and the correct order of columns.
Note: This feature is not accessible to users since it should be rarely invoked, only to reset the database to a known
state in the event of system failures :O
Some desired properties of the import to DB feature include:
- Ability to use exported csv files from /export-db as a backup
- Easy to use and debug (If I ever have to use this feature, I need it to be quick to minimise downtime)
Total Score >= 8 is a 'High Risk' for fall
History of fall within past 12 months
a. No fall (Score 0)
b. 1 fall prior to admission (Score 1)
c. 2 or more falls prior to admission (Score 5)
d. 1 or more falls during current admission (Score 5)
Cognitive status
a. Intact (Score 0)
b. Minimally impaired (Score 1)
c. Moderately impaired (Score 2)
d. Severely impaired (Score 3)
Continence problems
a. No continence problems or IDC in-situ (Score 0)
b. Incontinence of urine and/or faeces (Score 1)
c. Frequency (empties bladder > 6 times daily)/ Diarrhoea (Score 1)
d. Urgency (Score 1)
e. Needing nocturnal toileting more than 2 times daily (Score 1)
Safety Awareness
a. Good awareness and requests appropriate assistance (Score 0)
b. Occasional risk taking behaviours (Score 1)
c. Inappropriate fear for activities (Score 2)
d. Frequent risk-taking behaviours (Score 3)
Unsteadiness when standing, transferring and/or walking
a. Steady gait or complete dependent or on traction (Score 0)
b. Minimally unsteadiness which needs supervision (Score 1)
c. Moderately unsteadiness which require hands on assist at times (Score 4)
d. Severely unsteadiness and need constant hands on assist (Score 5)