About Halioooo
Halioooo is a Deep-Tech AI Ventures company focused on creating transformative solutions for exponential times. Their work spans three key areas:
-
Synthetic Sociology: Using AI to generate synthetic personas with complete life stories in order to interact with these personas for research purposes.
-
Health and Well-Being: They’re transforming healthcare with AI that adapts to each person, blending tech and biology to build stronger, smarter systems.
-
Green IT & AI: They create AI tools that are both powerful and environmentally friendly, setting new standards for sustainable tech.
The team is very focused on delivering fast without compromising the scalability of the systems, which is why collaborating together was a great fit.
The Challenge: Scaling Innovation at Halioooo
When I first began working with Halioooo they were just starting to build the first of many products and they needed to optimize the team's efforts by creating a solid foundation for their architecture and a set of reusable components.
Implementing this from the very beginning would mean that the cost saved from having to reimplement components or different common patterns across the projects would be far too expensive than investing in the from the start in a library of components and architecture.
The Solution: Architectural Patterns for Scalable Innovation
After analyzing Halioooo's specific challenges, we implemented two complementary architectural patterns: the Use Case pattern for organizing business logic and a Design System for user interfaces. These were chosen to address Halioooo's need to balance speed and innovation with consistency.
The Story Behind Use Cases and Design Systems
In software development, organizations like Halioooo constantly face the challenge of building applications that are maintainable, scalable, and consistent. As systems grow in complexity, the need for architectural patterns that can manage this complexity becomes increasingly important. I like applying two powerful patterns that I see as game-changers: the Use Case pattern and Design Systems.
These patterns aren't just theoretical concepts—they're practical tools that transform how teams build software. This case study explores how these patterns are implemented in our library for Halioooo and the tangible value they bring to organizations struggling with complex software challenges.
The Use Case Pattern: Clean Architecture in Action
What is the Use Case Pattern?
The Use Case pattern is a core component of Clean Architecture, popularized by Robert C. Martin (also known as "Uncle Bob"). At its heart, this pattern does something remarkably powerful: it separates business logic from implementation details, making code significantly more maintainable and testable.
Clean Architecture promotes the separation of concerns through concentric layers, with business rules at the center and implementation details at the outer layers. Use cases sit in the application layer, encapsulating business logic and orchestrating the flow of data between the domain entities and the outside world.
An use case represents how an user can interact with the application
This approach isn't just theoretical—it's practical and transformative.
Key Components
- Base Use Case Interface: The foundation of our implementation is the
UseCase
interface:
export interface UseCase<In = unknown, Out = unknown> { handle(param?: In, meta?: UseCaseOptions): Promise<Out> }
This simple interface defines a contract for all use cases: they take an input, optional metadata, and produce an output asynchronously.
Let's look at a complete implementation of a use case that fetches user data:
// Interfaces interface User { id: string; name: string; email: string; } interface GetUserParams { userId: string; } // Repository interface (dependency) interface UserRepository { findById(id: string): Promise<User | null>; } // Use case implementation export class GetUserUseCase implements UseCase<GetUserParams, User> { constructor(private readonly userRepository: UserRepository) {} async handle(params: GetUserParams): Promise<User> { const { userId } = params; // Validate input if (!userId) { // Errors can be replaced in the future with DomainErrors throw new Error('User ID is required'); } // Execute business logic const user = await this.userRepository.findById(userId); // Handle edge cases if (!user) { throw new Error(`User with ID ${userId} not found`); } return user; } } // Usage with dependency injection const container = new Container(); container.register('UserRepository', UserRepositoryImplementation); const useCaseService = container.get<UseCaseService>(UseCaseService.name); // Execute the use case const user = await useCaseService.execute(GetUserUseCase, { userId: '123' });
This example demonstrates several key benefits:
- Clear dependencies: The use case depends on a repository interface, not a concrete implementation which can be changed easily at compile time or even at run time
- Input validation: The use case validates its input parameters
- Error handling: The use case handles edge cases like missing users
- Dependency injection: The use case receives its dependencies through constructor injection which makes the code easier to change and to test
- Testability: The use case can be easily tested by mocking the repository
- Command and Query Separation (CQRS): We implement the CQRS pattern with two specialized interfaces:
export interface Command<Param = void, Return = void> extends UseCase<Param, Return> {} export interface Query<Return, Param = void> extends UseCase<Param, Return> {}
Commands are used for operations that modify state (with default void return), while Queries are used for retrieving data (requiring a return type). This separation follows the Command pattern from the Gang of Four design patterns, where commands encapsulate all the information needed to perform an action.
The CQRS pattern provides several benefits:
- Separation of concerns: Read and write operations often have different requirements and can be optimized separately.
- Scalability: Read and write operations can be scaled independently.
- Security: It's easier to apply different security rules to commands and queries.
- Simplicity: Each use case has a single responsibility, making it easier to understand and maintain.
- Middleware Chain: Our implementation uses the Chain of Responsibility pattern to apply cross-cutting concerns. This in my opinion is what sets the system apart, since it makes the application immensely robust and flexible. Let me show you:
// UseCaseService creates a chain of middlewares let next = UseCaseHandler.create({ next: this.container.create(useCase), options: requiredOptions, middleware: this.container.get<EmptyMiddleware>(EmptyMiddleware.name), }) for (let i = this.middlewares.length - 1; i >= 0; i--) { const currentMiddleware = this.middlewares[i] const previous = next next = UseCaseHandler.create({ next: previous, middleware: currentMiddleware, options: requiredOptions, }) }
The Chain of Responsibility pattern allows a request to pass through a chain of handlers, with each handler deciding whether to process the request and/or pass it to the next handler in the chain. This provides tremendous versatility in how use cases are executed.
Technical Flexibility of the Middleware Chain
The middleware chain offers several powerful capabilities:
-
Composability: Middlewares can be composed in any order, allowing for flexible combinations of cross-cutting concerns.
-
Order Matters: The order of middlewares in the chain affects how they process requests. For example:
- Placing a logging middleware at the beginning and end of the chain allows logging both before and after use case execution.
- Placing an error handling middleware early in the chain ensures it catches errors from all subsequent middlewares.
-
Conditional Processing: Middlewares can conditionally process requests based on the request type, metadata, or other factors.
// Example of a conditional middleware export class ConditionalMiddleware implements Middleware { async execute<In, Out>( param: In, next: UseCase<In, Out>, options: UseCaseOptions, ): Promise<Out> { // Check conditions based on param or options if (options.skipCondition) { // Skip this middleware and pass to the next one return next.handle(param, options) } // Process the request console.log('Conditional middleware processing request') // Continue the chain return next.handle(param, options) } }
-
Extensibility: New middlewares can be added without modifying existing code, following the Open/Closed Principle.
-
Aspect-Oriented Programming: Middlewares enable aspect-oriented programming by separating cross-cutting concerns from business logic.
-
Middleware Implementations: We provide several middlewares for common concerns:
- Logging: Captures information about use case execution for debugging and monitoring.
- Error handling: Catches and processes errors that occur during use case execution.
- Success handling: Processes successful results from use cases.
- Confirmation prompts: Requests user confirmation before executing certain operations.
- Server error handling: Handles errors specific to server communication.
Here's a real-world example of a logging middleware that measures execution time:
export class PerformanceLoggingMiddleware implements Middleware { constructor(private readonly logger: Logger) {} async execute<In, Out>( param: In, next: UseCase<In, Out>, options: UseCaseOptions, ): Promise<Out> { // Get the name of the use case being executed const useCaseName = next.constructor.name; // Record start time const startTime = performance.now(); try { // Log the beginning of execution this.logger.info(`Starting execution of ${useCaseName}`, { useCaseName, params: param, options, }); // Execute the next middleware or use case const result = await next.handle(param, options); // Calculate execution time const executionTime = performance.now() - startTime; // Log successful completion this.logger.info(`Successfully executed ${useCaseName} in ${executionTime.toFixed(2)}ms`, { useCaseName, executionTime, resultType: typeof result, }); return result; } catch (error) { // Calculate execution time even for failures const executionTime = performance.now() - startTime; // Log error with execution time this.logger.error(`Failed to execute ${useCaseName} after ${executionTime.toFixed(2)}ms`, { useCaseName, executionTime, error, }); // Re-throw the error to be handled by error middleware throw error; } } }
This middleware demonstrates how cross-cutting concerns like performance monitoring can be cleanly separated from business logic. By adding this middleware to your chain, you automatically get performance metrics for all use cases without modifying any of their code.
Organizational Value
The Use Case pattern doesn't just improve code—it transforms how organizations build and maintain software. Here's how:
-
Business Logic Isolation: By isolating business logic in use cases, organizations ensure their core business rules remain untangled from UI, database, or framework concerns. This isn't just good practice—it's a competitive advantage that follows the Single Responsibility Principle and maintains a clean architecture.
-
Superior Testability: Use cases are remarkably testable because they have clear inputs and outputs, and dependencies can be easily mocked. This enables comprehensive testing and supports Test-Driven Development (TDD), leading to more reliable software.
-
Enhanced Maintainability: When business requirements inevitably change, developers can modify specific use cases without affecting the entire system. This dramatically reduces the cost of change and makes the codebase more resilient to evolution, a critical factor in long-lived enterprise applications.
-
Team Scalability: Development teams can work on different use cases in parallel without conflicts. This natural alignment with Conway's Law allows organizations to scale their engineering teams effectively while maintaining productivity.
-
Accelerated Onboarding: New developers can quickly understand the system by reading use cases, which express business logic clearly. This significantly reduces the learning curve and accelerates developer productivity—a crucial factor in today's competitive hiring market.
-
Powerful Reusability: Well-designed use cases can be reused across different parts of the application or even different applications. This promotes DRY (Don't Repeat Yourself) principles and increases development efficiency across the organization.
Design Systems: Consistency and Efficiency
What is a Design System?
A design system is far more than just a UI library—it's a complete ecosystem of reusable components, guided by clear standards and principles, that can be assembled to build cohesive applications. It's the difference between building with random parts versus building with perfectly matched components designed to work together.
Our design system is thoughtfully implemented using a foundation of modern, battle-tested technologies:
- shadcn/ui: A collection of beautifully designed, accessible components
- Radix UI: A low-level UI component library focused on accessibility and customization
- Tailwind CSS: A utility-first CSS framework that enables rapid, consistent styling
This combination creates a powerful foundation that balances flexibility with consistency, allowing teams to move quickly without sacrificing quality.
Key Components
- UI Components: We provide reusable UI components like Button with various variants and sizes which are generated with shadcn:
const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all...", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90...", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground...", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", invisible: "whitespace-normal rounded-none", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, );
Here's the complete Button component implementation that uses these variants:
import { Slot } from "@radix-ui/react-slot"; import { type VariantProps, cva } from "class-variance-authority"; import type { ComponentProps } from "react"; import { cn } from "../lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", invisible: "whitespace-normal rounded-none", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); function Button({ className, variant, size, asChild = false, ...props }: ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> ); } export { Button, buttonVariants };
And here's how you would use this component in your application:
import { Button } from "@halioooo-studio/ui/button"; // Default button <Button>Click me</Button> // Different variants <Button variant="destructive">Delete</Button> <Button variant="outline">Cancel</Button> <Button variant="secondary">Secondary Action</Button> <Button variant="ghost">Ghost Button</Button> <Button variant="link">Learn more</Button> // Different sizes <Button size="sm">Small Button</Button> <Button size="lg">Large Button</Button> <Button size="icon">🔍</Button> // With icon <Button> <IconComponent /> With Icon </Button> // As a link <Button asChild> <a href="/destination">Link Button</a> </Button> // Disabled state <Button disabled>Disabled</Button>
This demonstrates the flexibility and consistency provided by our design system. With just a few props, developers can create buttons with different appearances and behaviors while maintaining a consistent look and feel across the application.
-
Documentation with Storybook: We use Storybook to document our components, making it easy for developers to see all available variants and how to use them. Storybook provides an isolated environment for developing UI components, allowing developers to browse a component library, view the different states of each component, and interactively develop and test components.
-
Theming and Customization: Our design system supports theming through CSS variables and Tailwind CSS, allowing organizations to customize the look and feel to match their brand. This approach enables:
- Consistent theming: All components use the same color palette, typography, and spacing.
- Dark mode support: Easy switching between light and dark themes.
- Brand customization: Organizations can override default variables to match their brand guidelines.
Organizational Value
Design systems don't just make applications look better—they fundamentally transform how organizations build digital products:
-
Unmatched Consistency: A design system ensures a unified user experience across all parts of an application and across different applications. This isn't just about aesthetics—it's about creating a coherent experience that users can navigate intuitively, reducing cognitive load and increasing user satisfaction.
-
Dramatic Efficiency Gains: When developers don't need to reinvent the wheel for common UI patterns, development speed increases dramatically. Teams can focus on solving unique business problems rather than debating button styles or rebuilding common components. This component-based approach can reduce UI development time by 30-50%.
-
Elevated Quality: Components in a design system are thoroughly tested and refined, leading to higher quality UIs with fewer bugs and edge cases. This supports shift-left testing by catching issues early, resulting in more robust applications and fewer production issues.
-
Built-in Accessibility: A well-designed system ensures that all components meet accessibility standards (WCAG), making applications usable by everyone including people with disabilities. This isn't just good practice—it's often a legal requirement and expands your potential user base.
-
Strengthened Brand Identity: A design system helps maintain a consistent brand identity across all digital products, strengthening brand recognition and user trust. This consistency creates a professional impression that elevates how users perceive your organization.
-
Enhanced Collaboration: Perhaps most importantly, design systems bridge the gap between designers and developers by providing a common language and set of components. This shared understanding, often implemented through design tokens, transforms cross-functional collaboration from a pain point to a strength.
The Results
What emerges when organizations adopt these patterns isn't just better code—it's a fundamentally transformed development process. Teams that implement the Use Case pattern and Design Systems consistently report:
- Reduction in bugs related to business logic
- Faster development of new features
- Significant improvements in code maintainability and developer onboarding
- Enhanced collaboration between design and development teams
These aren't just incremental improvements—they represent a step-change in capability that allows organizations to deliver more value to their users with less effort and higher quality.
Conclusion
The Use Case pattern and Design Systems represent more than just architectural choices—they're strategic investments in your organization's ability to build and maintain complex software. By implementing these patterns, you create a foundation that enables your teams to work with greater efficiency, higher quality, and improved collaboration.
The middleware chain in our Use Case implementation is particularly powerful, offering a flexible way to handle cross-cutting concerns while keeping the core business logic clean. By composing middlewares in different orders and creating custom middlewares for specific needs, organizations can adapt the system to their unique requirements without modifying the underlying architecture.
Our library provides a solid foundation for both patterns, with a clean implementation of the Use Case pattern and a growing design system. By leveraging these tools, organizations can focus on delivering value to their users rather than reinventing the wheel for common patterns and components.
These patterns aren't just theoretical—they're battle-tested approaches that have proven their worth across countless projects and organizations. They represent the distilled wisdom of decades of software engineering experience, packaged in a way that's accessible and immediately applicable to your challenges.
Ready to Transform Your Development Process?
If you're facing challenges with:
- Maintaining complex business logic across your application
- Ensuring consistency in your user interface
- Scaling your development team while maintaining code quality
- Reducing technical debt and improving maintainability
- Accelerating development without sacrificing quality
I'd love to discuss how these patterns can be applied to your specific challenges. Let's schedule a call to explore how your organization can benefit from implementing these architectural patterns.
During our call, we'll:
- Discuss your current development challenges
- Explore how the Use Case pattern and Design System can address those challenges
- Create a tailored roadmap for implementing these patterns in your organization
- Answer any questions you have about the implementation process
Don't let technical debt and inconsistent UIs slow down your development process. Take the first step toward a more maintainable, scalable, and efficient codebase today.