Composition Patterns
Once you treat Astro as the shell, the central design question is when the slices get combined. There are two answers, and a mature architecture often uses both: build-time integration and run-time integration.
Build-time integration: shared packages in a workspace
Section titled “Build-time integration: shared packages in a workspace”In build-time integration, each team publishes its slice as a versioned package. A monorepo with workspaces (npm, pnpm, or Yarn) is the most common home for this. The shell depends on @store/catalog, @store/cart, and so on, pins versions, and imports them like any other component:
---// The shell imports versioned team packages.import Catalog from '@store/catalog';import Cart from '@store/cart';---<Catalog client:visible /><Cart client:load />Because composition happens during the shell’s build, you get the things compilers are good at: type checking across slice boundaries, tree-shaking, and a single optimized output. The catch is independence — a Catalog change only reaches users once the shell bumps the version, rebuilds, and redeploys. Build-time integration favours safety and consistency over instant, autonomous shipping.
Run-time integration: compose when it is requested
Section titled “Run-time integration: compose when it is requested”In run-time integration, each slice is a separately deployed artifact. The shell does not import it at build; it pulls it in when the page is rendered (on the server) or after it loads (in the browser). Teams ship on their own clock, and the shell picks up the new version on the next request — no shell redeploy required. The price is weaker compile-time guarantees and more operational surface, covered in detail in the next lesson.
flowchart TD
subgraph BuildTime["Build-time integration"]
BT1["Team pkg @store/cart v1.4"] --> BT2["Shell build imports it"]
BT2 --> BT3["One optimized bundle"]
end
subgraph RunTime["Run-time integration"]
RT1["Team deploys cart service"] --> RT2["Shell fetches at request / in browser"]
RT2 --> RT3["Composed live, no shell rebuild"]
end Server islands: deferred per-team fragments
Section titled “Server islands: deferred per-team fragments”Astro’s server islands give you a powerful middle ground. By adding server:defer to a component, you tell Astro to render the rest of the page immediately and stream that island in afterwards from the server. Each island can have its own fallback slot shown while it loads:
---import Recommendations from '../islands/Recommendations.astro';---<Recommendations server:defer> <p slot="fallback">Loading recommendations…</p></Recommendations>For micro-frontends this is ideal: a slow or personalized team fragment — recommendations, a cart badge, a live price — no longer blocks the rest of the page. The fast, cacheable shell ships first, and each team’s deferred island arrives on its own. It is per-team laziness without any client-side framework cost for the fallback.
Try it: the shell importing a team component
Section titled “Try it: the shell importing a team component”The runnable page below is a minimal composition shell. It imports one “team” component and renders it as a section — the same shape you would use whether the import resolves to a workspace package or a local slice.
---
// src/pages/index.astro — composition shell
// In a real setup, ProductCard would come
// from a versioned package like @store/catalog.
import ProductCard from '../components/ProductCard.astro';
---
<html lang="en">
<head><title>Composition shell</title></head>
<body>
<header><h1>Acme Store</h1></header>
<main>
<h2>Catalog team's slice</h2>
<ProductCard
name="Astro Mug"
price="12.00"
/>
</main>
<footer>Composed by the Astro shell.</footer>
</body>
</html>For this to run you would add src/components/ProductCard.astro that reads Astro.props (name, price) and renders the card. The shell does not care how the slice is built internally — only what props it accepts.