Software architecture is often referred to as the highest level of system abstraction. And software design to the lower-level details of the individual modules and their responsibilities. But actually, both low-level details and high-level decisions are part of the whole design that produce our software system.
In the previous part, we discussed the background and the software architecture – the high-level abstraction of the protests map application. In this part, we will dive into the lower-level details and specify technologies and libraries we will use in our app.
Unlike the architecture, which is meant to be agnostic to the implementation details, This document is all about these details. And so, the details we want to hide in the architecture, become the central part of our discussion. Let’s start by specifying our database.
Database Specification
Most enterprise software designers will choose a relational database management system (RDBMS) as their default data storage. There are good reasons for it, the relational model is elegant, disciplined, and it is an excellent data storage and access technology.
While relational tables may be convenient for certain forms of data access, there is nothing architecturally significant about arranging data into rows within tables. Our application use cases should neither know nor care about such matters.
Structuring our data model as a table schema would be as simple as structuring it as an object in document storage.
{
id: string;
icon: string;
location: {
lat: number;
lng: number;
};
userId: string;
createdAt: number;
lastModified: number;
}
id | icon | lat | lng | userId | createdAt | lastModified |
uuid | varchar | float | float | fk_user | date | date |
The thing is that RDBMSs are too big of an effort for such a small project. The benefits of storing our data on disks, the fancy operations that come with RDBMS are not helpful if the app is made of a tiny data schema.
Instead of a fancy database system, we want a managed solution that will prevent us from complicated deployments, integrations and other complexities of database administration, so we can focus on building fast and deploying easily our application to production.
Almost every cloud provider suggest one or more managed database solutions. From DigitalOcean to Heroku, through AWS and GCP. For the virtual demonstration application, I chose Firestore for the following reasons:
- Realtime updatesThe requirement: <em>These views should get updated upon new flags being added</em>.
- Serverless backend Firebase provides a full backend solution that minimizes dramatically the work needed to produce our app.
- Pricing Firebase allows a free quota of reads, writes, deletes. As well as hosting and auth
- Security and setupFirestore provides an easy setup for documents and security rules.
- Firebase hostingFirebase provides also hosting and authentication services.
Firestore comes with event listeners that are listening to write operations. When performing a write operation, these listeners will be notified of the new data before the data is sent to the backend. We will see later how Angular translated this feature of Firestore to an observable subject.
Once we have chosen Firestroe as our database/backend server we can re-design our high-level diagram.
HIgh-level Diagram
As mentioned in the above list, Firebase provides a serverless backend solution. That means that we don’t need a mid-level tier for reading and writing from/to the database. Meaning, our software architecture becomes 2-tiers architecture (or simply a single-tier – client app), and will look like this:
Client Specification
Angular and Firebase are a perfect pair, in Angular v13 you can even create a Firebase project from Angular CLI by simply running:
ng add @angular/fire
This command will take you through all the Firebase set-up processes, from creating a project and Firestore/Realtime database to adding authentication service, setting a hosting configuration and deploying your application. Angular is an open-source project, it provides a great selection of third-party integrations other than Firebase, that can be added to the framework with ease. Among those third-party integrations, is Google Maps, which we will be using in our project.
Reactive Programming – Observables and Behaviour Subjects
To satisfy our functional requirement – These views should get updated upon new flags being added, we need a real-time mechanism that listens to changes and updates our component upon said events. In other words, we need a dynamic data stream that facilitates the automatic propagation of the changed data flow.
Angular makes use of the RxJS observables to handle several common asynchronous operations. Such as HTTP AJAX requests and responses, the famous 2-way binding of forms is made of observables listening to user-input events. Actually, the Angular EventEmitter is an observable subject, and probably the most relevant to our little application is the @angular/fire library which implements observables for Firebase services.
As it goes along very well, we will also be using the RxJS BehaviourSubject for our state page store. The BehaviourSubject is a variant of RxJS Subjects – the special type of observable, that allows multicasting values to many observers. Additionally to the subject kind of observable, it has the notion of “current value”, meaning that every time an observer subscribes to the BehaviourSubject, it will immediately receive the current value from the BehaviorSubject.
This allows us to pipe new values of our collection into our state page store upon changes while assigning the changes together with our state. The following code is demonstrating the page store abstraction and a subscription to the collection observable from the flag service constructor:
// pagestore factory class
export abstract class PagestoreAbstract<T> {
protected bs!: BehaviorSubject<T>;
state$: Observable<T>;
state: T;
previous?: T;
protected abstract pagestore: string;
constructor(initialValue: Partial<T>) {
this.bs = new BehaviorSubject<T>(initialValue as T);
this.state$ = this.bs.asObservable();
this.state = initialValue as T;
this.state$.subscribe((s) => {
this.state = s;
});
}
patch(newValue: Partial<T>): void {
this.previous = this.state;
const newState = Object.assign({}, this.state, newValue);
this.bs.next(newState);
}
}
// flag state interface
export interface FlagState {
loading: boolean;
formStatus: string;
flags: Flag[];
myFlags: Flag[];
liveFlags: Flag[];
timeFilter?: {
createdAt: number;
};
colorFilter?: {
url: string;
};
hasLiveFlag?: boolean;
totalFlags: number;
totalLiveFlags: number;
totalUserFlags: number;
totalUniqueUsers: number;
error?: string;
action?: string;
}
// flag's pagestore instance
export class FlagPagestore extends PagestoreAbstract<FlagState> {
protected pagestore = 'flag';
constructor() {
super({
loading: true,
formStatus: '',
flags: [],
myFlags: [],
liveFlags: [],
totalFlags: 0,
totalLiveFlags: 0,
totalUserFlags: 0,
});
}
}
export class FlagService {
constructor(
private firestore: FlagFirestore,
private pagestore: FlagPagestore
) {
this.firestore
.collection$()
.pipe(
map((flags) => {
this.pagestore.patch(
{
loading: false,
flags,
liveFlags: this.getLiveFlags(flags),
hasLiveFlag: this.getHasLiveFlags(flags),
myFlags: flags.filter((flag) => flag.uid === this.uid),
totalFlags: flags.length,
totalLiveFlags: this.getTotalLiveFlags(flags),
totalUserFlags: flags.filter((flag) => flag.uid === this.uid).length,
totalUniqueUsers: this.getTotalUniqueUsers(flags),
}
);
})
)
.subscribe();
}
// now we can read our data from the flag's page store, which is cheaper and faster than database calls.
}
The same way we do with authentication service, the firebase authentication provides an observable object of the currently signed-in user or null. But since the user object is provided by firebase, and we don’t have anything to do with the user information, the only thing we need is the user id. So we don’t need to store the user in a Firestore collection.
Before we move on to the actual code, let’s have a look at the client components we will build.
Application Modules
Angular has its system and glossary and the Angular Module term is different from the JavaScript term, if you’re not familiar with NgModules, you can read about it in the documentation.
The following lists the modules in our app:
- AppModuleApplication root
- CoreModuleFirestore configuration, page store, Firestore and authentication abstract classes
- FlagModuleFlag service, flag page store and Firestore implementation, flags page and flag form components
- UserModuleUser service, user page store, auth implementation, sign in and sign up components
- LayoutModuleCustomeIcon registry, navbar, errorMatcher, errorSnackbar
Client High-level Diagram
Next, we will walk through the implementation of our app.