Vertical slice
A vertical slice is a feature shipped through every layer it touches - UI, data fetch, feature flag, analytics, persistence - in one narrow strip rather than building each layer broadly first and stitching later. For growth engineering, every test variant is a vertical slice whether you call it one or not.
The opposite is horizontal slicing. The team agrees to “do all the components first”, “then wire up the data”, “then add tracking”, “then put it behind a flag”. Each layer is broad. Nothing renders end-to-end until the last week. The CRO calendar slips into the next quarter.
Why this matters for growth work
Section titled “Why this matters for growth work”A test is a unit of learning. Until the variant is rendered, tracked, flagged, and analysable, nothing has been learned. Horizontal slicing optimises for engineering ergonomics - you write all the React components in a row, all the API routes in a row, all the schemas at once. The work feels efficient. The slice that proves the hypothesis arrives months late, or not at all.
Vertical slicing accepts repetition. Five separate, similar-ish features will each implement their own thin version of the same layers, and the abstraction gets pulled out at the third or fourth occurrence. That feels wasteful and it isn’t. The cost of a wrong abstraction extracted from one feature is bigger than the cost of repeating yourself across three.
A worked example: headless Next.js + Shopify
Section titled “A worked example: headless Next.js + Shopify”The hypothesis: showing a “Frequently bought together” module on PDPs lifts AOV. The team is on a Next.js storefront fronted by Shopify’s Storefront API.
The slice has to touch:
- A flag, so the module is only shown to the treatment group.
- A data fetch, because recommendations come from somewhere.
- A component, to render the products with price, image, and add-to-cart.
- Three events - module impression, item click, item add-to-cart.
- A mutation against Shopify’s cart.
The horizontal approach builds a Recommendations Service over three sprints, a generic ProductCard component, an instrumentation layer for “module-style” tracking. Nothing ships.
The vertical approach builds the thinnest version of each layer. About 150 lines of code across four files. Two days of work. Behind a flag, ready to flip to 50% of traffic.
The flag gate
Section titled “The flag gate”import { evaluateFlag } from '@/lib/experiments'import { FrequentlyBought } from './FrequentlyBought'
export default async function ProductPage({ params }) { const product = await getProduct(params.handle) const variant = await evaluateFlag('pdp_fbt_v1', { userId: await getUserId(), })
return ( <> <Product product={product} /> {variant === 'treatment' && ( <FrequentlyBought productId={product.id} variant={variant} /> )} </> )}The flag call happens on the server, which kills flicker - the HTML arrives with or without the module, no client-side swap. The variant value is passed into the module so events can be attributed correctly.
The data fetch
Section titled “The data fetch”async function getFbtProducts(productId: string) { const res = await shopifyFetch({ query: GET_FBT_PRODUCTS, variables: { productId }, next: { tags: [`fbt:${productId}`], revalidate: 3600 }, }) return res.data.product.recommendations.slice(0, 3)}Cached for an hour, revalidated on demand. The first version doesn’t need a recommendation engine - Shopify’s productRecommendations query returns related products. Ship that, iterate later. If the slice proves the hypothesis, the v2 is “swap the data source for our own model”. The component, the flag, the events all stay.
The render and impression event
Section titled “The render and impression event”export async function FrequentlyBought({ productId, variant }) { const products = await getFbtProducts(productId) if (products.length === 0) return null
return ( <Impression event="fbt_module_viewed" properties={{ product_id: productId, variant, count: products.length }} > <section> <h2>Frequently bought together</h2> {products.map((p, i) => ( <FbtItem key={p.id} product={p} position={i} sourceProductId={productId} variant={variant} /> ))} </section> </Impression> )}<Impression> is a client wrapper that fires its event when the section enters the viewport. The other two events (fbt_item_clicked, fbt_added_to_cart) live in <FbtItem>.
The add-to-cart action
Section titled “The add-to-cart action”'use client'async function addFbtToCart(productId: string, sourceProductId: string, variant: string) { track('fbt_added_to_cart', { product_id: productId, source_product_id: sourceProductId, variant, }) await addToCart({ merchandiseId: productId, quantity: 1 })}The event fires before the network call, in case the user navigates away mid-mutation. The variant rides along on every event so the warehouse can compute the AOV lift without joining back to the assignment table.
That’s the slice. Variant traffic is being assigned, the module renders for half the visitors, every meaningful action is instrumented. Nothing was built for hypothetical future modules. When the next test wants a similar shape, copy this file and change five things - extract the shared parts at the third or fourth instance, not the first.
Where vertical slicing gets misused
Section titled “Where vertical slicing gets misused”- Slicing too thin. A “slice” that doesn’t actually answer a question - no events fire, the flag is fake, the data is mocked - isn’t a slice. It’s a UI prototype with extra steps.
- Skipping the boring layers. Tracking is the layer most often left for “next sprint”. The variant ships, the data is incomplete, the test is unanalysable. Tracking belongs in the slice, not after it.
- Refactoring the slice before the second one exists. Pull abstractions when there are two or three examples of the same thing. Not before. The premature
RecommendationsModulebase class will be wrong for the next test that needs to vary something it didn’t anticipate. - Slicing a spike. A spike (a throwaway exploration) doesn’t need a full slice. Don’t ship analytics or flags for code that’s going to be deleted next week.
The link to test velocity
Section titled “The link to test velocity”Experiment velocity is mostly a function of how fast a hypothesis becomes a deployed, tracked variant. Teams that vertical-slice ship five tests in the time a horizontal team takes to ship one. The difference compounds over a year into a different programme entirely.
Most CRO debt - the kind covered in technical debt as a CRO problem - is the residue of horizontal builds. The recommendations service that took six months and was never used. The analytics layer that was perfect for events nobody ever wired up. Vertical slicing isn’t a CRO concept by origin but it’s the working pattern that lets a CRO programme stay productive.