Domain Modelling
Now that we have the requirements of the app, let’s start with defining the domain models.
Domain models are the essence of an app. If the models are defined as fitting as possible, it will make implementing the business logic more seamless later. If the models are designed to be as generic as possible, it will make it less painful to change the implementation later.
Of course, we cannot possibly predict everything that will happen during the course of implementation, and we need to be prepared that some of the initial designs will have to change. Which is why, it will be better if we do not make too much assumption about what web framework or datastore mechanism will be used for actual implementation during the modelling stage.
Having said that, for the convenience of documenting the domain models, we will use json
data type for key-value or
array type fields. Not all datastores can support JSON data type natively, and if that is the case, then we will map
the key-value or array type field to a separate model.
Similarly, for the convenience of documentation, we use ERD which implies the use of a relational database, but we should be able to use any type of datastores if we need to.
With that, let’s get into the details of the models that we have identified for this app.
Let’s get going with the design.
Transactions
At the core of the app is all about Transactions. There are a few types of transactions:
- Income Transactions: These are transactions as a result of receiving payments for offering services or products.
- Expense Transactions: These are transactions as a result of making payments for another party’s services or products.
- Income Reversal Transactions: These are transactions as a result of reversing an income transaction.
- Expense Reversal Transactions: These are transactions as a result of reversing an expense transaction.
By default, home currency amount will be automatically calculated based on the exchange rate between the home Currency and the transaction’s Currency. However, the user should also have the flexibility to overwrite the amount if there’s a specific amount determined by the service provider.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Transactions { text id PK text type "not null" date transaction_date "not null" text description "not null" numeric amount "not null" numeric home_currency_amount "not null" json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" text currency_id FK "not null" text associated_transaction_id FK }
Tags
One of the critical features of this app is the use of Tags. Tags can be used to filter Work Logs to generate Invoices, or render charts, or use for tax computation. For this reason, we model Tag as an explicit entity.
Each Tag can have a category and a name. The pair needs to be unique as it will form the composite
primary key of each record. By default, the system will render the Tag with the format
category:name
. The user can customise the format under System Config.
For display distinction, the user can customise the color scheme of each Tag. By default, the system will render all Tags with white text on grey background, like this company:company-abc.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Tags { text category "not null, default: ''" text name "not null, composite PK and unique constraint: [category, name]" text description text text_color "default: 'white'" text background_color "default: 'grey'" timestamp created_at "not null" timestamp updated_at "not null" }
Work Logs
Another main feature of the app is the capability to track work done so that the time logs can be used to generate Invoices to another party.
For invoicing purpose, the user can input a short description. If she wants to log the details of the work done, she can do so with the content field.
The user can specify more than one Tag associated with the Work Log.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram WorkLogs { text id PK timestamp start_time "not null" timestamp end_time "not null" text description "not null" text content json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" }
Invoice Configs and Invoice Templates
In order to generate Invoices, the user can configure the properties of the Invoice to each individual party.
The tags field tells the system to filter only specific Work Logs with those Tags to be included in the generated Invoice.
The custom fields will be propagated to each instance of Invoices when it’s generated. This allows the user to configure fields that can be used in the Invoice Template to display additional information that is not directly related to the Work Logs.
In order to render the Invoice, the user can choose from built-in templates or create custom template using some form of markup and styles languages (we do not make any assumption at this point regarding the actual languages to use).
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram InvoiceConfigs { text id PK text description "not null" text invoice_cycle_date "not null, default: 1" text invoice_cycle_duration_value "not null, default: 1" text invoice_cycle_duration_unit "not null, default: 'month'" numeric due_date_cycle_value "not null, default: 30" text due_date_cycle_unit "not null, default: 'day'" text payment_terms json tags "not null, default: '[]'" json custom_fields "not null, default: '{}'" timestamp created_at "not null" timestamp updated_at "not null" text invoice_number_sequence_id FK "not null" text billable_contact_id FK "not null" text invoice_template_id FK "not null" } InvoiceTemplates { text id PK text name "not null" text content_markup "not null" text content_styles "not null" timestamp created_at "not null" timestamp updated_at "not null" }
Invoices and Invoice Lines
Then each instance of the Invoices generated will be tracked individually. Each Invoice can contain one or more lines for the calculation of the total sum.
By default, the Invoice Lines are generated from the Work Logs using the predefined Billing Configs for the billing Contact. If the user needs to include additional costs, then she can create new Invoice Lines accordingly, and use the Invoice Template to customise the display of these custom Invoice Lines.
There is also another possible scenario where the Invoice doesn’t involve any Work Log. In this case, the user can simply manually create an Invoice and custom Invoice Lines.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Invoices { text id PK text invoice_number "not null" date invoice_date "not null" date due_date "not null" numeric total_amount "not null" json custom_fields "not null, default: '{}'" bool voided "not null, default: false" timestamp created_at "not null" timestamp updated_at "not null" text invoice_config_id FK "not null" text currency_id FK "not null" text contact_id FK "not null" } InvoiceLines { text id PK text description "not null" text unit "not null" numeric unit_cost "not null" numeric unit_value "not null" numeric subtotal "not null" timestamp created_at "not null" timestamp updated_at "not null" text invoice_id FK "not null" }
Income Receipt Configs and Income Receipt Templates
Alongside the Invoices, the user can also configure the properties of the Income Receipt as a result of payment based on the issued Invoice.
The custom fields in Income Receipt Configs will be propagated to each instance of Income Receipt when it’s generated. This allows the user to configure fields that can be used in the Income Receipt Template to display additional information that is not directly related to the associated Invoice.
In order to render the Income Receipt, the user can choose from built-in templates or create custom template using markup and styles languages.
All fields from the associated Invoice can be used in the Income Receipt Template in addition to the Income Receipt Config fields.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram IncomeReceiptConfigs { text id PK json custom_fields "not null, default: '{}'" timestamp created_at "not null" timestamp updated_at "not null" text receipt_number_sequence_id FK "not null" text billable_contact_id FK "not null" text income_receipt_template_id FK "not null" } IncomeReceiptTemplates { text id PK text name "not null" text content_markup "not null" text content_styles "not null" timestamp created_at "not null" timestamp updated_at "not null" }
Income Receipts
Then each instance of the Income Receipts generated will be tracked individually. It is possible that the Invoice is paid in multiple instalments, and there will be more than one Income Receipt associated with each Invoice.
The system will automatically populate some of the amount fields as such:
- billable_amount: Copied from the Invoice’s total amount.
- paid_amount: Starts with 0, each subsequent entry will copy from the previous entry’s payment amount.
- payment amount: Input by the user.
- remaining amount: Calculated by billable amount - paid amount - payment amount.
However, the user will have the flexibility to overwrite the amount fields where applicable, for example, if the payment is received through a third party service provider and the Income Receipt is automatically generated by that provider. In this case, there may not be an associated Income Receipt Config or Invoice.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram IncomeReceipts { text id PK text receipt_number "not null" date receipt_date "not null" numeric billable_amount "not null" numeric paid_amount "not null" numeric payment_amount "not null" numeric remaining_amount "not null" json custom_fields "not null, default: '{}'" bool voided "not null, default: false" timestamp created_at "not null" timestamp updated_at "not null" text income_receipt_config_id FK text invoice_id FK text currency_id FK "not null" text income_transaction_id FK "not null" text contact_id FK "not null" }
Expense Receipts
While Income Receipts are created when the user’s business receives payments from a client, and the payment proof is issued in the form of a Receipt, Expense Receipts are received when the user makes payment to external parties. Since the nature of these Receipts are different, we make a distinction in the modelling.
The fields in the Expense Receipt are also simpler, as they only need to mirror the Expense Transaction fields for record keeping purpose.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram ExpenseReceipts { text id PK text receipt_number "not null" date transaction_date "not null" numeric amount "not null" numeric home_currency_amount "not null" timestamp created_at "not null" timestamp updated_at "not null" text currency_id FK "not null" text expense_transaction_id FK "not null" }
Sequences
The user may configure the number Sequences to use in the Invoice Templates and Receipt Templates. Prefix and suffix fields are available, so that the user can customise the Invoice number or Receipt number in the Template accordingly.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Sequences { text id PK text name "not null" numeric starting_number "not null, default: 1" numeric last_used_number "not null" numeric increment_step "not null, default: 1" text prefix text suffix timestamp created_at "not null" timestamp updated_at "not null" }
Contacts and Billing Configs
To simplify the configuration of Invoices and Receipts, the user can create Contacts, which contain important information such as name, address and account number, then use them in the Invoices or Receipts configuration.
The user may configure a unique billing structure for each client, eg. charging by an hourly rate for work done, a fixed cost for a project of various complexities. When generating an Invoice from Work Logs, the Billing Configs will be used to generate the Invoice Lines accordingly and perform the calculation where applicable.
For now, there will be 3 rate types:
- duration: calculate the sub-total using the Work Log duration.
- count: calculate the sub-total using the number of Work Log entries.
- fixed: a fixed value regardless of the number of Work Log entries.
Note that the user can also create her company contact as an entry, and then use it in the System Config.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Contacts { text id PK text name "not null" text account_number text address_line_1 text address_line_2 text address_line_3 text city text state text country text postcode text contact_number_1 text contact_number_2 text contact_number_3 text contact_person_1 text contact_person_2 text contact_person_3 text contact_email_1 text contact_email_2 text contact_email_3 text url_1 text url_2 text url_3 blob logo json custom_fields "not null, default: '{}'" timestamp created_at "not null" timestamp updated_at "not null" } BillingConfigs { text id PK text description "not null" date effective_start "not null" date effective_end text rate_type "not null, default: 'duration'" text unit "not null, default: 'hour'" numeric unit_cost "not null" json include_tags "not null, default: '[]'" json exclude_tags "not null, default: '[]'" text contact_id FK "not null" }
Currencies
The user may need to setup different Currency exchange rates for the purpose of calculating Income or Expense Transactions in another Currency back to her home Currency for tax computation.
Note that the exchange rates are relative to USD for standardisation purpose, which means for the entry for USD, the
exchange rate should be stored as 1
(this will be loaded automatically by default at the outset). If the user’s home
Currency is not USD, then two step conversion will be applied. For example, if the user’s home
Currency is GBP, and the client Currency is JPY, then the system will convert JPY to
USD, then from USD to GBP.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Currencies { text id PK text code "not null" text symbol "not null" numeric exchange_rate "not null" date effective_start "not null" date effective_end timestamp created_at "not null" timestamp updated_at "not null" }
Documents
The user may generate Invoices or upload Receipts, and tag them for categorisation purpose, and later compile them using the Tag filters as proof of documentation for tax filing purpose.
Note that throughout the development of the app, it is possible to change this model to not store the file content directly, but to have a reference link to an external storage, eg. Amazon S3, local file system.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Documents { text id PK text filename "not null" text mime_type "not null" blob content "not null" json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" }
Invoice Documents / Receipt Documents
For each Document uploaded, the user can associate it to a particular Invoice or Receipt. And each Invoice or Receipt can have zero or more Document associated.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram InvoiceDocuments { text id PK text description "not null" json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" text document_id FK "not null" text invoice_id FK "not null" } IncomeReceiptDocuments { text id PK text description "not null" json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" text document_id FK "not null" text income_receipt_id FK "not null" } ExpenseReceiptDocuments { text id PK text description "not null" json tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" text document_id FK "not null" text expense_receipt_id FK "not null" }
Recurring Tasks and Recurring Task Steps
There’s a feature to allow the user to setup recurring Transactions, and this can be done by creating Recurring Tasks that detail the schedule (represented in cron format) of the task.
There will be built-in task templates that the user can choose from to setup each Recurring Task. Each task can be composed of one or more step, and each step will be executed according to the defined rank. The smaller the rank number, the earlier they get executed, and in sequence. If there are steps with the same rank, they will be executed in parallel.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram RecurringTasks { text id PK text description "not null" text cron_schedule "not null" date effective_start "not null" date effective_end bool enabled "not null, default: true" timestamp created_at "not null" timestamp updated_at "not null" } RecurringTaskSteps { text id PK text description "not null" numeric rank "not null" json params "not null, default: '{}'" text task_step_template_id "not null" timestamp created_at "not null" timestamp updated_at "not null" text recurring_task_id FK "not null" }
Recurring Task and Step Runs
For each time the Recurring Task is run, we will record the run for troubleshooting purpose.
Each step also has its own run record, so that we can easily resume or retry particular steps as a recovery mechanism.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram RecurringTaskRuns { text id PK timestamp start_time "not null" timestamp end_time text status "not null, default: 'pending'" timestamp created_at "not null" timestamp updated_at "not null" text recurring_task_id FK "not null" } RecurringTaskStepRuns { text id PK timestamp start_time "not null" timestamp end_time text status "not null, default: 'pending'" timestamp created_at "not null" timestamp updated_at "not null" text recurring_task_run_id FK "not null" text recurring_task_step_id FK "not null" }
Tax Tables, Tax Tiers and Tax Deductibles
In order to compute the tax payable, the user needs to setup the Tax Table accordingly. As tax rates may change over the years, the user can create multiple instances of Tax Tables for each applicable period.
For each Tax Table, there will be a number of Tax Tiers that specify the income range and tax rate for each tier.
There are also Tax Deductibles related to business expenses that can be added as part of the tax computation.
The include tags and exclude tags allow the user to specify what types of Transactions should be included or excluded from the computation.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram TaxTables { text id PK text description "not null" date effective_start "not null" date effective_end json include_tags "not null, default: '[]'" json exclude_tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" } TaxTiers { text id PK numeric min_income "not null" numeric max_income "not null" numeric max_payable_amount "not null" numeric rate "not null" timestamp created_at "not null" timestamp updated_at "not null" text tax_table_id FK "not null" } TaxDeductibles { text id PK text category "not null" text description "not null" text type "not null" numeric rate numeric max_deductible_amount json include_tags "not null, default: '[]'" json exclude_tags "not null, default: '[]'" timestamp created_at "not null" timestamp updated_at "not null" text tax_table_id FK "not null" }
Alerts
There’s a feature that allows the user to setup a reminder for each Recurring Task. These will be Alerts that appear in the inbox, and the latest Alert will also be displayed as a banner in the app.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Alerts { text id PK text title "not null" text content "not null" bool is_read "not null, default: false" timestamp created_at "not null" timestamp updated_at "not null" }
Chart Configs
The user can configure what charts to display in the analytics page. Each chart has a display order, the lower the number, the earlier it is rendered on the page. If there are multiple charts with the same display order, then they will be rendered on the same row where possible, or will wrap around to the next row if not.
The user can select which data source to use for rendering the chart. There’ll be 2 options for now:
- Transactions: summation by amount
- Work Logs: summation by duration
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram ChartConfigs { text id PK text description "not null" text chart_type "not null, default: 'bar'" text data_source "not null" text scale "not null, default: 'month'" text group_by "not null" json include_tags "not null, default: []" json exclude_tags "not null, default: []" date start_date "not null" date end_date "not null" numeric display_order "not null, default: 1" bool active "not null, default: true" timestamp created_at "not null" timestamp updated_at "not null" }
System Configs
Finally, the user can setup System Config that applies to the entire app. There can be multiple instances of the System Config, so that the user can change the config at different period in time.
Tags can be formatted using category and name, and for convenience, we will use the %{}
syntax to
represent variables interpolation. However, we do not make assumption about the templating framework we will use at
this stage, and in the end it may turn out that we will use a framework with a different syntax, eg. Liquid template
language, and the syntax will change accordingly.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram SystemConfigs { text id PK date effective_start "not null" date effective_end text tag_format "default: '%{category}:%{name}'" text timezone "default: 'UTC'" timestamp created_at "not null" timestamp updated_at "not null" text base_currency_id FK "not null" text base_contact_id FK "not null" }
Users
Finally, there’s the Users that we need to store to support multi-user login feature.
At this stage of the design, we don’t want to make too much assumption about the authentication mechanism, so we will start with a password-based authentication. Note that we will encrypt the password before storing, but for simplicity we will just call the field “password” instead of “encryption_password”.
For security reason, we may evolve to using more complex encryption, eg. salted encryption, and then we will need to add a field to store the salt, or we could be using other authentication mechanisms, eg. OAuth, and we can evolve this model accordingly.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#c87137" } } }%% erDiagram Users { text id PK text username "not null" text password "not null" text first_name text last_name timestamp created_at "not null" timestamp updated_at "not null" }
That’s All
If you have read this far, you would have noticed that there’s some business logic hidden in the description of the domain models. That resonates with the statement earlier at the start of this article, where I stated that fitting domain modelling can make implementing business logic more seamless.
Data are the soul of a system, and by looking at the data models, one should be able to guess what sort of system they belong to, and even deduce what kind of logic is implemented in the system.
Note that I used the word “fitting” here rather than “accurate”, because domain modelling is an art as much as it is a science, different engineers may imagine the domain in slightly different ways, and the resulting domain models may look slightly different, so there’s no one accurate answer to the design. However, we can say that there will be certain commonalities among various designs because these are obviously relevant to the domain, eg. Transactions.
That’s about all the domain models that we have identified and designed in details for now. Let’s move on to designing the user journey of the app!