Addendum: Building a Practice Management System

Addendum: Building a Practice Management System

If your organization is building a practice management system, the following guidance will help you build a future-proof application that will not need constant maintenance and refactoring as your client base grows.

Database Design

Use "long" Primary Keys

Avoid string values or guids as these often lead to performance issues and index fragmentation.
Avoid composite keys.

Use Relational Databases

Relational databases with strict schemas do a great job of keeping bad data out of your application.
On No-SQL/JSON/XML-based databases, we often see that it is easy for XML/JSON data to be stored that is in the incorrect schema or of an incorrect type.  (For example, it is easy for the boolean value true to accidentally be stored as the string value "true".


Generally Choose *-to-Many Relationships

One of the most common areas that practice management vendors have to refactor as their client base grows is the cardinality of relationships.
For example, many systems start with only allowing a Contact record to have:
  1. 1 Email Address
  2. 2 Phone Numbers (Home/Work)
  3. 1 Postal Address
But then later need to refactor their app to support contacts with any number of email addresses, phone numbers, and postal addresses.

Plan for Hierarchical Category Objects

There are many different object types that can be through of as categories such as:
  1. Practice Areas
  2. Contact Types
  3. Document Types
  4. Calendar Event Types
  5. Task Types
When building your database, you should allow nested categories.  For example, you should support the following:
  1. Practice Area:  Family Law > Divorce > With Children
  2. Contact Type: Providers > Medical > Pediatric
  3. Document Types: Motions > Private Process
  4. etc.

Minimize *Required* Inter-Object Dependencies

As you build your application, you should do so in a way that makes as many relationships optional as possible.
For example, your application will likely have contacts, matters, practice areas, and documents.
You should be able to:
  1. Store a matter without having a client
  2. Store a document without associating it to a matter.

Plan for Notes on Any Object

Many new-to-market systems start out with:
  1. a MatterNotes table.
  2. Then they add a ContactNotes table
  3. and an InvoiceNotes table
  4. and a DocumentComments table
  5. and a DocumentVersionComments table
  6. then a TimeEntryInternalComments table
  7. and the list goes on.
Instead, create a Notes table that supports attaching notes to any kind of object.

Plan for Custom Fields on any Object and of any Object Type 

Many new-to-market systems start out with:
  1. Supporting custom text-line fields on matters
  2. Then they add custom integer fields, custom checkbox fields, custom money fields, custom decimal fields, and custom text-area fields, and custom URL fields.
  3. Then they add custom picklist fields, custom checklist fields
  4. Then they allow custom fields to be attached to contacts
  5. Then they allow custom fields to be attached to documents
  6. Then they allow a custom field to be a contact-picklist, matter-picklist, user-picklist or document picklist.
  7. Then they allow a custom field to be a task picklist, a calendar entry picklist, and more.
Avoid all that refactoring and build a database that supports all of the above.

Always Soft-Delete

All records (contacts, matters, notes, etc.) should have one of the following statuses:
  1. Active
  2. Archived
  3. Draft
  4. SoftDeleted
When records are soft-deleted, it allows you to solve two problems:
  1. If the customer deletes something unintentionally, restoring is easy.
  2. When a consumer of your API is synchronizing, it is easy to detect soft deletes but very hard to detect hard deletes.
    1. For example, if a record has been soft-deleted, a developer can tell your API "Show me anything that has been modified since my last synchronization, including deleted records".  If you only support hard-deletes, then the request will be "Give me everything so I can detect what you've deleted."

Store Audit Information

All records should have the following fields:
  1. Created_At / Created_By
  2. Updated_At / Updated_By
  3. Deleted_At / Deleted_By

Reference Codes and DisplayOrders

In many systems, objects have ReferenceCodes and DisplayOrders.
You should support these on all object types.

ReferenceCodes are non-numeric identifiers/abbreviations used by humans to identify a record.
Some examples of reference codes are:
  1. Matter "Numbers" (they're not numbers)
  2. Invoice "Numbers" (they're not numbers)
  3. Document Type Codes (ie. MOT = Motion)
  4. Practice Area Codes (ie. FAM = Family Law)
DisplayOrders are human-assigned custom sort orders.
Some examples of DisplayOrders being used are:
  1. Custom Sorting Practice Areas
  2. Custom Sorting Picklist Items

Don't Store People and Companies in Different Tables

Some systems do this.  It causes issues for their customers because if a contact has the wrong type, it can't easily be changed.
Instead, use one table, and have a Kind property that indicates whether the contact is a Person or a Company.

Don't Have multiple Contact Tables

You should not have different tables for Clients, Vendors, Opposing Parties, etc.
Instead, you should implement ContactCategories and allow a contact to be associated to 0 or more ContactCategories.


Design Rate Classes Correctly

A lot of new-to-market systems really mess up their rate class implementation.
  1. They will start with users having a default rate.
  2. Then they'll allow per-client overrides
  3. Then they'll allow per-matter overrides
This creates significant problems for firms because they lack a global view of their firm's rates and any customizations have to be manually implemented in mass. (For example, imagine having to manually change the rates on each one of AAA Insurance's matters to the negotiated 2025 rates.)


A good rate class implementation should consist of two object types:
  1. Rate Class
  2. Rate Class Rule
The Rate Class object should have the following properties:
  1. Id
  2. ParentRateClassId
  3. Name
This way rate classes can be nested inside of each other like this:
  1. Default Rate Class
    1. Family Law
    2. Litigation
    3. Insurance Defense
      1. AAA
    4. Customized Rates
      1. Smith, John
      2. Doe, Jane
        1. JD.001
        2. JD.002
The Rate Class Rule object should have the following properties:
  1. RateClassId
  2. DisplayOrder
  3. Date_From
  4. Date_Till
  5. User/Group/Activity/UtbmsCode
  6. Rate
  7. RateType (Hourly/Fixed)
In this way the RateClassRules support the following "statements":
  1. The rate for Paralegals is $100/hr
  2. From 2022-2025, the rate for Paralegals is $150/hr
  3. From 2022-2025, the rate for Jessical Paralegal is $175/hr
  4. All A102 work is $200/hr
It should be possible to link a contact and a matter to a rate class.

When determining the rate for an activity:
  1. First, find the rate class to use.
    1. If a Matter has a rate class, use that rate class
    2. Otherwise, fall-back to the Client's rate class
    3. Otherwise fall back to the Default Rate Class
  2. Then Find the Rate Class Rule to use
    1. Then make a list of all of that rate class's parent rate classes in reverse order
      1. ie: AAA < Insurance Defense < Default Rate Class
    2. Starting with the current rate class, (ie. AAA)
      1. Get a list of that Rate Class's rules, sorted by Reverse DisplayOrder.
      2. For each rule, if the rule applies, set that rule as the Matching Rule and then stop searching
  3. Compute the rate using the Matching Rule.

App Design

Push - Don't Pull

The best applications store all their data in their own database.  They may push data to remote systems and utilize data in the remote system, but they never treat that remote system as the source of truth.
For example, if you are using a permissions API, your app should store all the data in your database and then push the relevant information into the remote system.  Once the data is pushed, you might query it directly from the remote system, but, in a disaster situation, you should be able to delete the remote system and rebuild it from the data in your database.

Documents, Versions, and Sharding

In your app, you will likely store documents, and their revisions in Amazon or Azure.  Whatever system you choose, your database should have its own documents and versions table: you should not rely on Azure or AWS's built-in versioning system.

When you store document versions, they should be stored with zero important metadata on them.  Your database should have everything it needs in order to directly download a document without needing to query anything in the underlying storage system.
For example, you might have a document version stored at any of the following locations:
  1. SuperSecureDocs.s3.AmazonAws.com/1/2024-03-1/1/2/3/4
  2. SuperSecureDocs.s3.AmazonAws.com/1/3/2/7/1/1
  3. SuperSecureDocs.s3.AmazonAws.com/1/5455bcdd-329d-4388-97cd-bed453b09d92
You'll notice that the document blob has no extension on it or anything.






    • Related Articles

    • 02 - Building APIs in the Best Order

      When building your API, you should think about object dependencies and build the top-level items first. For example, your application may have the following data types: Users Contacts Practice Areas Matters (Requires Contacts and Practice Areas) Time ...
    • Installing SQL Server Management Studio (SSMS)

      This article will walk you through installing and configuring SQL Server Management Studio (SSMS). If you have not already installed SQL Server, you should do so prior to installing SSMS. Visit ...
    • Creating a System Command Prompt

      For some advanced processes you may need to run a command prompt as the System user account. These instructions will guide you through this process. Creating a System Command Prompt Download PSTools from: ...
    • Push Connector Requirements.

      Universal Migrator facilitates migrations between hundreds of different applications into destination applications whom we have built a push connector for. If you are reading this article, you likely want Universal Migrator to load data into your ...
    • How to Configure SQL Server RAM Limits

      This article will guide you through setting up RAM limits for Microsoft SQL Server to ensure optimal performance while leaving sufficient resources for the operating system and other applications. Launch SQL Server Management Studio (SSMS) Click Open ...