Skip to Content
PatternsArchitecture

5-Layer Architecture

Every Mukoko app enforces a strict 5-layer component hierarchy. Components import from the layer below, never sideways or upward. This ensures isolation, testability, and prevents cascading failures.

Layer Hierarchy

LayerNamePath
L1Shared Primitivescomponents/ui/
L2Domain Compositescomponents/weather/, components/reports/
L3Page Orchestratorscomponents/landing/, components/dashboard/
L4Error Boundaries + Loading Statescomponents/section-error-boundary.tsx
L5Server Page Wrappersapp/[route]/page.tsx

Imports flow downward only.

Live demonstration

This live demo shows all 5 layers in action. Each section is wrapped in a SectionErrorBoundary — click “Trigger error” to see how one section crashes without affecting the others.

Click to crash the Analytics Chart section
Harare
Clear
Current conditions

28°C

Confidence92%
Farming — Maize harvest
Travel — Victoria Falls
Sports — Cricket match
Weekly temperature trend
Flood warningAlert
Crop reportNew
Road conditionsUpdated
L1 Primitives
L2 Composites
L3 Orchestrator
L4 Error Boundary
L5 Page Wrapper

Layer 1: Shared Primitives

Path: components/ui/

Foundational UI components from this registry — Button, Input, Card, Badge, etc. Installed via the shadcn CLI. Never import from higher layers.

components/ui/button.tsx
import { cva } from "class-variance-authority" import { cn } from "@/lib/utils" import * as Slot from "radix-ui/internal/slot" const buttonVariants = cva( "inline-flex items-center justify-center ...", { variants: { variant: { default: "bg-primary text-primary-foreground", outline: "border-border bg-input/30", }, size: { default: "h-9 px-3", sm: "h-8 px-3", }, }, defaultVariants: { variant: "default", size: "default" }, } ) export function Button({ className, variant, size, asChild, ...props }) { const Comp = asChild ? Slot.Root : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> ) }

Layer 2: Domain Composites

Path: components/weather/, components/reports/

Feature-specific components that compose Layer 1 primitives into domain-relevant UI. A WeatherCard combines Card + Badge + Progress from the registry.

components/weather/forecast-card.tsx
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" export function ForecastCard({ location, temp, condition, confidence }) { return ( <Card> <CardHeader> <CardTitle>{location}</CardTitle> <Badge variant="outline">{condition}</Badge> </CardHeader> <CardContent> <p className="text-2xl font-bold text-foreground">{temp}°C</p> <Progress value={confidence} className="mt-2" /> <span className="text-xs text-muted-foreground"> {confidence}% confidence </span> </CardContent> </Card> ) }

Layer 3: Page Orchestrators

Path: components/landing/, components/dashboard/

Compose Layer 2 components into full page sections. Orchestrators NEVER hardcode rendering logic — they import and arrange domain composites.

components/dashboard/weather-dashboard.tsx
import { ForecastCard } from "@/components/weather/forecast-card" import { ActivityFeed } from "@/components/reports/activity-feed" import { WeatherChart } from "@/components/charts/weather-chart" export function WeatherDashboard({ forecasts, activities, chartData }) { return ( <div className="grid gap-6 lg:grid-cols-3"> <div className="lg:col-span-2 flex flex-col gap-4"> {forecasts.map((f) => ( <ForecastCard key={f.id} {...f} /> ))} </div> <div className="flex flex-col gap-4"> <WeatherChart data={chartData} /> <ActivityFeed items={activities} /> </div> </div> ) }

Layer 4: Error Boundaries + Loading States

Path: components/section-error-boundary.tsx

Every section gets wrapped in a SectionErrorBoundary. If a chart crashes, only that section shows an error — the rest of the page continues working. This is mandatory.

components/dashboard/safe-dashboard.tsx
import { SectionErrorBoundary } from "@/components/section-error-boundary" import { WeatherOverview } from "@/components/weather/overview" import { ActivityFeed } from "@/components/reports/activity-feed" import { WeatherChart } from "@/components/charts/weather-chart" export function SafeDashboard({ data }) { return ( <main className="flex flex-col gap-6"> <SectionErrorBoundary section="Weather Overview"> <WeatherOverview forecast={data.forecast} /> </SectionErrorBoundary> <SectionErrorBoundary section="Activity Feed"> <ActivityFeed items={data.activities} /> </SectionErrorBoundary> <SectionErrorBoundary section="Weather Charts"> <WeatherChart data={data.chartData} /> </SectionErrorBoundary> </main> ) }

Layer 5: Server Page Wrappers

Path: app/[route]/page.tsx

Next.js App Router page.tsx files. Handle SEO metadata, data fetching, and pass props down to the orchestrator. These are React Server Components by default.

app/dashboard/page.tsx
import { Metadata } from "next" import { SafeDashboard } from "@/components/dashboard/safe-dashboard" export const metadata: Metadata = { title: "Dashboard — mukoko weather", description: "Real-time weather intelligence for your region.", } async function getWeatherData() { // Server-side data fetching const res = await fetch("https://api.mukoko.com/weather", { next: { revalidate: 900 }, // 15-minute cache }) return res.json() } export default async function DashboardPage() { const data = await getWeatherData() return <SafeDashboard data={data} /> }

Rules

  • Components import from the layer below, never sideways or upward
  • Each component is a standalone file — independently installable via the registry
  • Page orchestrators NEVER hardcode rendering logic — they compose imported components
  • All colors and styles come from CSS custom properties in globals.css
  • SectionErrorBoundary wrapping is mandatory for every page section
  • Server components by default — add “use client” only when using hooks or event handlers
Last updated on