Big Apps:
Mono-app,

Mono-repo,

Mico-UI; oh my!

L&L March 2022
Steve Olsen

GOALS:

  • To learn something new
  • Smile twice (or more)

Ways to shave

Pros and cons

  • No "one right" answer!
  • Tradeoffs abound
  • Implementation details mater, too!

# The problem

What's all the ado?

Sharing code is hard

  • Dependencies * (intra-product)
  • Code reuse
  • Feature vs. "Bug"
  • Breaking changes
  • Versioning
  • Hosting
  • Permissions

Modules (internal)

vs.

Libraries (external)

Dependencies

Mono-app

Mono-repo

Micro-UI

Module

Library

Goals:

  • Higher dev productivity

  • Easier code reuse/sharing (where it makes sense)

  • Less code breakage

iframe

Tell Me More.

# whynotlibs

i.e. "highly-decoupled code"

Theory vs. Practical

  • In theory, they:
    • are separate/decouple concerns
    • are highly specialized
    • work well
  • In practice, they:
    • break dependants easily
    • get mis-versioned
    • become low-visibility, "high-wall"
    • go stale/ "broken window" syndrome
    • mask teams' communication problems

For an example experience, see:
https://www.youtube.com/watch?v=0_qhdOeMuhk

Fit and purpose

Libs are best used for generic, highly-reusable, non-business related data problems.


Eg. formatters for dates/numbers, animations, etc.

*General tools, where any dependants don't affect them.*

# UX 101

Product Pipeline!

  1. "We want omnisearch in SuperApp"
  2. "We want SnapMoney widgets in SuperApp"

PR: link

Engineering Response:

  1. see "boxes"
  2. identify "patterns"

Modules!

"so many modules!"

Mobile

Desktop

Parts

*widget: smart components

*libs: pure functions/code

*layouts: adapt to their container size

# Namespacing and Refactoring 101

Sharing is caring!

Namespacing via file path

  • /branch-a/
  •     /branch-b/
  •         /leaf/
  •             index.ts

branchA.branchB.leaf

 

import @/branch-a/branch-b/leaf

VERY important how scoping is defined...

Page-scoped component


/src/
  /ui/
    /pages/
      /checkout/
        index.ts
        Page.ts     << imports ./select-box-auto
        /select-box-auto/...

Common-scoped component


/src/
  /ui/
    /common/
      /components/
        /select-box-auto/...
    /pages/
      /checkout/
        index.ts
        Page.ts     << imports ../../common/components/select-box-auto
      /sign-up/
        index.ts
        Page.ts     << imports ../../common/components/select-box-auto

Page-scoped widget


/src/
  /ui/
    /pages/
      /checkout/
        index.ts
        Page.ts     << imports ./cart
        /cart/...

Common-scoped widget


/src/
  /ui/
    /common/
      /widgets/
        /cart/...
    /pages/
      /checkout/
        index.ts
        Page.ts     << imports ../../common/widgets/cart
      /orders/
        index.ts
        Page.ts     << imports ../../common/widgets/cart

Specificity

  • /parent-a/
  •     /parent-b/
  •         /child/
  •             index.ts

generalize

specialize

# Monos

all your code is share to us
// Assuming NextJS

package.json
/pages/...    <<< routing {URL:Page} and {URL:API}
/src/
  /api/...      <<< define HTTP API handlers
  /common/      <<< eg. libs, frmt, types, validators, etc.
    /libs/
  /ui/
    /common/...   <<< Atlas, etc.
      /widgets/
      /components/
      /libs/
    /[vertical]/
      /pages/...
      /widgets/...
      /components/...

Mono-apps

folder structure

// Assuming yarn workspaces

package.json
/apps/         <<< self bundled / deployable units
  /web-uni/...
  /web-money/...
  /web-travel/...
  /web-shop/...
/packages/     <<< sharable units
  /@web-common/...    <<< eg. libs, types, validators, etc.
  /@web-[vertical]/
    /pages/...
    /widgets/...
    /components/...

Mono-repos

Each /apps/* deploys its own bundle

folder structure

Eg. Assemble web-uni!

packages/@web-uni/nav

packages/@web-uni/nav

app/web-uni/pages

packages/
@web-snapmoney/ components/
cta-activate/...

packages/
@web-snaptravel/ widgets/
bookings/...

packages/
@web-snapshop/ widgets/
daily-deals/...

packages/
@web-common/ components/
cta-ad/...

# Micro-UI

<html>
  <head>
    <script src="./app/main.js"></script>
    <script src="./app/nav.js"></script>
    <script src="./app/travel.js"></script>
    <script src="./app/shop.js"></script>
    <script src="./app/money.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Snap it Together

verticals = travel, shop, money, .....

import React, { Suspense, lazy } from "react";

...

const MoneyPage = lazy(() => import("./pkg-money/Home"));

...

<Suspense fallback={<div>Loading...<div>}>
  <Switch>
    <Route exact={true} path="/money" component={MoneyPage} />

Suspense + lazy

ES "async loading" other packages

Composition Example

Module Federation

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const mode = process.env.NODE_ENV || 'production';

module.exports = {
  mode,
  entry: './src/index',
  devtool: 'source-map',
  optimization: {
    minimize: mode === 'production',
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [ /* ... */ ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
    new ModuleFederationPlugin({
      name: 'remote', // this name needs to match with the entry name
      // ...
    }),
  ],
};
// Assuming yarn workspaces

package.json
/packages/
  /@pkg-main-app/
  /@pkg-common/	 <<< eg. libs, types, validators, etc.
  /@pkg-[vertical]/
    /pages/
    /widgets/
    /components/

Mono-repo for a Micro-UI!

Each package is self-hosted and deployed

Folder structure

Multi-repos for a Micro-UI!

Note: Libs/Components:

  • have low coupling/ no co-dependencies
  • are self-hosted and deployed
  • are imported/referenced by other pkgs
<html>
  <head>
    <script src="./app/main.js"></script>
    <script src="./app/nav.js"></script>
    <script src="./app/travel.js"></script>
    <script src="./app/shop.js"></script>
    <script src="./app/money.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Bonus Dev: Local vs. proxy to staging

localhost

localhost

stagging

Resources

# Events

Logout event

  1. User clicks logout
  2. Send analytics?
  3. Show notification?
  4. Tell Profile Page?
  5. Tell Cart Widget?
  6. Tell ...?

Pub/sub

Logout!

Pub/sub

Logout!

Some Challenges:

  • Event naming and payload definitions get complicated
  • Fundamental integration (i.e. longterm commitment!)
  • Event chaining can be difficult
    • Eg. call1, call2, call3
    • Eg. call1_pending, call1_success, call1_fail

Suggested Libs

  • useReducer hook
  • Vegemite
  • Redux Toolkit
  • Redux Sagas

THANK YOU

Questions?

"Good architecture has big benefits, but it needs: education, discipline, and enforcement. That is part of the Sr./Staff engineer role."

Big Apps

By solsen-tl

Big Apps

  • 181