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.
Design Choices Driven Primarily by Trends or Popularity
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.