Ctrl + K

The Philosophy of Pulsar web

How to think as a Pulsar developer

Philosophy

Pulsar is built around a clear and intentional philosophy. It is not designed to maximize flexibility or to fit every possible use case. Instead, Pulsar prioritizes clarity, predictability, and architectural integrity over convenience.

Every design decision in Pulsar exists to reduce cognitive load as applications grow, keeping UI behavior explicit and easy to reason about over time.

Core Principles

Explicit over Implicit

Dependencies, data flow, and state changes must be visible in code. Hidden behavior creates systems that are hard to understand and harder to debug.

// ✅ Explicit: Dependencies are visible
final class UserProfile extends Component {
  final User user;
  final AuthService auth;
  
  UserProfile(this.user, this.auth);  // Dependencies passed explicitly
}

// ❌ Implicit: Hidden dependencies
final class UserProfile extends Component {
  @override
  Morphic render() {
    final user = GlobalState.currentUser;  // Where does this come from?
    final auth = ServiceLocator.get<AuthService>();  // Hidden dependency
  }
}

Objects over Functions

Components are long-lived objects with identity, not stateless functions recreated on every render. This makes lifecycle clear, state predictable, and behavior testable.

// ✅ Objects: Identity preserved, state persists
final class Dashboard extends Component {
  int activeTab = 0;  // State lives in the object
  final AnalyticsWidget analytics = AnalyticsWidget();  // Identity preserved
  
  void switchTab(int index) {
    morph(() => activeTab = index);
  }
}

// ❌ Functions: Identity lost, state external
Widget dashboard() {
  return Column([AnalyticsWidget()]);  // New instance every call
}

Configuration over Magic

Behavior should be configured explicitly, not inferred from conventions or hidden rules. Magic frameworks are pleasant until they're not.

// ✅ Configuration: Explicit setup
Router(
  routes: [
    Route(path: '/', builder: (_) => HomePage()),
    Route(path: '/about', builder: (_) => AboutPage()),
  ],
  notFound: () => NotFoundPage(),
)

// ❌ Magic: Convention-based routing
// Files in pages/ automatically become routes
// Behavior inferred from file structure

Architecture over Convenience

Good architecture may require more code upfront, but it pays dividends as systems grow. Convenience-driven patterns often create technical debt.

// ✅ Architecture: Separated concerns
class ShoppingCart {  // Domain logic
  double get total => /* ... */;
}

class CartStyles {  // Styling logic
  String get classes => /* ... */;
}

final class CartView extends Component {  // UI structure
  final ShoppingCart cart = ShoppingCart();
}

// ❌ Convenience: Everything in one place
final class Cart extends Component {
  double get total => /* ... */;  // Business logic
  String get classes => /* ... */;  // Styling logic
  @override
  Morphic render() => /* ... */;  // UI structure
}

Long-lived UI over Ephemeral Renders

Components should preserve identity and state across renders. Recreating components on every render creates unpredictable behavior and makes state management complex.

// ✅ Long-lived: Component stored as field
final class Page extends Component {
  final ChartWidget chart = ChartWidget();  // Created once, reused
  
  @override
  Morphic render() {
    return Div()([chart]);  // Same instance always
  }
}

// ❌ Ephemeral: Component recreated every render
@override
Morphic render() {
  return Div()([ChartWidget()]);  // New instance every time
}

Domain Oriented UI (DOU)

At the heart of Pulsar's philosophy is Domain Oriented UI (DOU) — a methodology inspired by Domain-Driven Design (DDD) applied to frontend development.

The Core Insight

Frontend code often mixes three distinct concerns:

  • Business Logic — Rules, calculations, validations
  • Styling Logic — Visual decisions, conditional classes, theme adaptation
  • UI Structure — What appears on screen, in what order

Traditional component systems encourage mixing all three. DOU separates them:

  • Domain Objects — Encapsulate business and styling logic
  • Components — Describe pure UI structure

DOU in Practice

// Domain Object: Business Logic
class OrderCalculator {
  final List<OrderItem> items;
  
  OrderCalculator(this.items);
  
  double get subtotal =>
    items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
  
  double get shipping => subtotal > 50 ? 0 : 5.99;
  double get tax => subtotal * 0.1;
  double get total => subtotal + shipping + tax;
  
  bool get qualifiesForFreeShipping => subtotal > 50;
}

// Domain Object: Styling Logic
enum OrderStatus { pending, processing, shipped, delivered }

class OrderStyles {
  final OrderStatus status;
  final bool qualifiesFreeShipping;
  
  OrderStyles({required this.status, required this.qualifiesFreeShipping});
  
  String get statusClasses {
    return switch (status) {
      OrderStatus.pending => 'order-status order-pending',
      OrderStatus.processing => 'order-status order-processing',
      OrderStatus.shipped => 'order-status order-shipped',
      OrderStatus.delivered => 'order-status order-delivered',
    };
  }
  
  String get shippingBadgeClasses =>
    qualifiesFreeShipping ? 'badge badge-success' : 'badge badge-muted';
}

// Component: Pure UI Structure
final class OrderSummary extends Component {
  final OrderCalculator calculator;
  final OrderStatus status;
  
  OrderSummary(this.calculator, this.status);
  
  OrderStyles get styles => OrderStyles(
    status: status,
    qualifiesFreeShipping: calculator.qualifiesForFreeShipping,
  );

  @override
  Morphic render() {
    return Div().classes("order-summary")([
      Div().classes(styles.statusClasses)([
        'Status: ${status.name}',
      ]),
      
      if (calculator.qualifiesForFreeShipping)
        Span().classes(styles.shippingBadgeClasses)(['Free Shipping!']),
      
      Div().classes("order-totals")([
        P()(['Subtotal: $${calculator.subtotal.toStringAsFixed(2)}']),
        P()(['Shipping: $${calculator.shipping.toStringAsFixed(2)}']),
        P()(['Tax: $${calculator.tax.toStringAsFixed(2)}']),
        P().classes("total")(['Total: $${calculator.total.toStringAsFixed(2)}']),
      ]),
    ]);
  }
}

The component is a thin coordinator. Business rules and styling decisions are isolated, testable, and reusable.

Why DOU Matters

  • Testability — Test business logic without rendering UI
  • Reusability — Use OrderCalculator in API responses, background jobs, tests
  • Clarity — Components describe what, domain objects describe how
  • Maintainability — Change business rules without touching UI code
  • Type Safety — Enums and objects prevent invalid states

DOU vs Traditional Component Patterns

// Traditional: Everything in component
final class OrderSummary extends Component {
  List<OrderItem> items = [];
  OrderStatus status = OrderStatus.pending;
  
  double get subtotal =>
    items.fold(0.0, (sum, item) => sum + item.price * item.quantity);
  
  double get shipping => subtotal > 50 ? 0 : 5.99;
  
  String get statusClasses {
    return switch (status) {
      OrderStatus.pending => 'order-status order-pending',
      // ... more logic
    };
  }
  
  @override
  Morphic render() {
    // 100+ lines mixing business logic, styling, and structure
  }
}

// DOU: Separated concerns
// - OrderCalculator: 30 lines of business logic, testable
// - OrderStyles: 20 lines of styling logic, testable
// - OrderSummary: 20 lines of pure UI structure
// Total: 70 lines, all isolated and focused

DOU Principle: Components should be so simple that they barely need tests. Domain objects should be so isolated that they're easy to test. When logic lives in the right place, testing becomes natural.

Objectives & Vision

Pulsar aims to provide a framework where applications remain understandable months or years after they are written. It encourages explicit data flow, stable component identity, and clear separation of responsibilities.

Long-term Thinking

Most frameworks optimize for the first week of development. Pulsar optimizes for the second year.

  • Code is read more than written — Clarity pays dividends over time
  • Teams change — New developers should understand the codebase
  • Requirements evolve — Architecture should accommodate change
  • Bugs are expensive — Explicit patterns prevent entire classes of bugs

Architectural Integrity

Pulsar guides you toward patterns that scale:

  • Components with single, clear responsibilities
  • Domain objects that encapsulate logic
  • Explicit dependencies that make testing easy
  • Type-safe abstractions that prevent invalid states
  • Clear boundaries between concerns

What Pulsar Intentionally Avoids

Hidden State Propagation and Implicit Reactivity

Automatic reactivity creates systems where it's unclear what triggers what. In Pulsar, state changes are explicit via morph().

Short-lived Components Recreated on Every Render

Components that are created and destroyed on every render make state management complex and behavior unpredictable. Pulsar components are long-lived objects.

Global Stores as a Default Architectural Requirement

Global state is a last resort, not a starting point. In Pulsar, state lives in components or domain objects, passed explicitly through the tree.

Framework Abstractions that Obscure How the Web Works

Pulsar embraces HTML, CSS, and browser APIs. It does not try to create a proprietary abstraction layer over the web platform.

Pulsar's design is driven by principles, not trends. If a pattern is popular but creates technical debt, Pulsar will not adopt it.

Note: Pulsar does not aim to replace every existing solution or to compete on ergonomics alone. If Pulsar feels restrictive, that is by design. The framework favors coherence over universality.

Constraints as Guides

Every constraint in Pulsar exists to guide you toward maintainable patterns:

  • No logic in render() → Forces you to organize code in testable getters
  • Components stored as fields → Makes identity and lifecycle explicit
  • morph() for state changes → Makes updates visible and deliberate
  • No global state → Encourages explicit data flow
  • Enums over strings → Prevents entire classes of typo bugs
  • Domain objects for logic → Isolates business rules from UI

These constraints may feel limiting at first, but they eliminate entire categories of bugs and architectural problems.

The Pulsar Way

Pulsar is not just a framework — it's a way of thinking about frontend architecture:

  • Treat components as long-lived objects, not disposable functions
  • Separate business logic from UI structure
  • Make dependencies explicit, never hidden
  • Use type systems to prevent invalid states
  • Prefer clarity over cleverness
  • Design for the team that will maintain the code in two years

If you adopt these principles, Pulsar becomes natural. If you fight them, Pulsar will feel restrictive.

Philosophy Summary: Pulsar believes that frontend applications deserve the same architectural rigor as backend systems. Domain-Driven Design, explicit dependencies, and type safety are not just backend concerns — they make frontend code better too.