Assignment complete #1
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Solace Advocate Search Improvements
This document outlines the backend and data schema changes I made to improve scalability, maintainability, and performance for the Solace assignment. We'll start with the backend and data model, then cover the frontend in a later section.
NOTE: The goal of this exercise is to communicate my ability and proficiency in building systems. I am a big fan of small, digestible pull requests. I felt that making lots of small PRs with the amount of improvements I wanted to do would make it very hard to communicate the goal of this exercise. It would be taxing to ask someone to glue 5-8 small PRs in their head, all while trying to understand all the changes. I decided to spend more time making this doc and stepping through everything I did.
Introduction
The original backend schema used a flat structure with embedded JSONB arrays for specialties and stored city and degree as plain text fields. While this works for small datasets, it does not scale well for larger, relational data or for efficient querying and filtering. My changes focus on leveraging relational database best practices, improving query performance, and making the API more maintainable and extensible. I learned early on that you are not smarter than the people who wrote PostgreSQL. This is relational data, and relational databases handle relational data really, really well.
Backend & Data Schema Changes
1. Normalized Data Model
Original Schema Example
My Improved Schema
advocatestable.advocate_specialties).cityIdanddegreeIdas FKs, and their specialties are joined via the linking table.Why Normalize?
Why Not JSONB for Specialties?
2. API Endpoints for Reference Data
cities,degrees, andspecialties(e.g.,/api/cities,/api/degrees,/api/specialties).3. Caching for Static Data
cities,degrees, andspecialtiessince this data rarely changes.4. Advocate Query Improvements
/api/advocatesendpoint supports filtering by all fields, including specialties, cities, degrees, and years of experience.Counting Results
SELECT COUNT(*)query with the same filters.5. Indexing for Fast Search
pg_trgmextension onfirst_nameandlast_nameto support fast, case-insensitive substring search (ILIKE '%name%').varcharovertextfor first and last name fields to semantically indicate these are short strings. In PostgreSQL, both types are stored inline for small values, but usingvarcharmakes the intent clearer and can help prevent accidental misuse for large text data. If a largetextvalue is accidentally stored, PostgreSQL will move it to thepg_toasttable for out-of-line storage, which adds overhead and can negatively impact query performance6. Preventing Too Many Database Connections in Development
During development, I encountered the error
PostgresError: sorry, too many clients already. This happens because, in environments with hot reloading (like Next.js), the backend code can be re-executed multiple times, causing a new Postgres client to be created on each reload. As a result, the database quickly hits its connection limit, leading to this error.To fix this, I used the
globalThisobject to store the Postgres client and Drizzle instance. Before creating a new client, the code checks if one already exists onglobalThisand reuses it if available. This ensures that only a single connection pool is maintained during development, preventing connection leaks and excessive client creation. This pattern is widely recommended for Node.js/Next.js projects and is safe becauseglobalThispersists across module reloads in development, but not in production.Summary
Frontend & UX Improvements
The original frontend was a simple React page with a single search box, a reset button, and a table that displayed all advocates. Filtering was done entirely on the client, and all data was fetched and held in memory. While this works for small datasets, it does not scale and does not provide a modern, user-friendly experience.
1. Advanced Filtering & UX Rationale
Specialty, Degree, and Location Filters:
I introduced multi-select dropdowns for specialties, degrees, and cities. This allows users to filter advocates by any combination of these attributes.
OR Logic: I chose to implement all filters (specialties, degrees, and locations) as an "OR" condition. This means advocates are shown if they match any of the selected specialties, any of the selected degrees, or any of the selected cities. I intentionally avoided mixing "AND" and "OR" logic between filter types, as this can be confusing for users, especially since some fields (like location and degree) are mutually exclusive for a single advocate. Keeping all filters as "OR" conditions provides a consistent and predictable experience, similar to what users expect from major platforms like Amazon.
Pillboxes for Active Filters:
I added pillboxes to visually represent each active filter. This provides immediate feedback to the user about what filters are applied and allows for quick removal of any filter by clicking the "X" on the pill. This is a familiar pattern from e-commerce and search UIs, reducing cognitive load and making the interface more intuitive.
2. Pagination for Scalability
The original implementation fetched all advocates and filtered them on the client. This approach does not scale; fetching and rendering millions of records is not feasible and would result in poor performance and user experience.
I implemented server-side pagination, fetching only the advocates needed for the current page. This keeps the UI fast and responsive, regardless of the total dataset size, and is a best practice for scalable web applications. We can control how beefy our servers are, but we can't control the specs of the client's computer. Always better to have control and push the heavy lifting on systems we can control.
3. Multi-Select Dropdowns
I built a reusable
MultiSelectFiltercomponent for specialties, degrees, and cities. This component supports selecting multiple options, displays the count of selected items, and is keyboard accessible.Multi-select dropdowns are a familiar and efficient way for users to select multiple filters without cluttering the UI with dozens of checkboxes.
4. Branding & Visual Design
I configured Tailwind CSS to use the Solace brand color (
#347866) throughout the UI for buttons, pillboxes, and highlights. This ensures a cohesive and professional look that matches the company's identity. One concern I did not address with the colors is any a11y issues with the color scheme. It might not be a11y friendly for people with red/green color blindness.I extracted the SVG logo from the Solace website and created a reusable React component for it, placing it prominently in the page header.
5. Other UX Enhancements
The UI always shows which filters are active, and users can remove any filter with a single click.
The filter section and table are responsive and look good on various screen sizes.
All form controls are labeled, and the dropdowns are keyboard navigable.
I chose not to implement column sorting, as most fields are text-based and do not lend themselves to meaningful ordering. The only numeric field, years of experience, can be filtered by setting a minimum value, which is more useful for this context.
6. Specialties Display in the Table
Reducing Visual Noise:
Initially, displaying all specialties for each advocate in the table made the UI cluttered and overwhelming, especially for advocates with many specialties. To improve readability and reduce noise, I truncated the list to show only the first two specialties by default. If an advocate has more specialties, a "+N more" link appears, allowing users to expand and view the full list on demand. This keeps the table clean and focused, while still making all information accessible.
Prioritizing Filtered Specialties:
When a user filters by specialty, advocates who match the filter may have many specialties. To make the filtered results more meaningful and user-friendly, I biased the display order so that the specialties matching the user's filter appear first in the truncated list. This ensures that the most relevant information is immediately visible, and users can quickly see why a particular advocate matched their search criteria, even before expanding the full list.
7. Summary of Frontend Improvements