Space-Based Architecture
Space-Based Architecture is a distributed architectural pattern where application functionality is broken down into independently deployable services, often referred to as “spaces.” These spaces are designed to be loosely coupled, communicating primarily through well-defined APIs and asynchronous messaging. Each space owns its data and can be scaled and updated independently, promoting agility and resilience. This contrasts with monolithic architectures or tightly coupled service-oriented architectures.
This pattern is particularly useful for large, complex applications that require high scalability, fault tolerance, and rapid development cycles. It’s well-suited for microservices implementations, event-driven systems, and applications that need to adapt quickly to changing business requirements. The independent nature of spaces allows teams to work autonomously and deploy updates without impacting other parts of the system.
Usage
Space-Based Architecture is commonly used in:
- E-commerce Platforms: Separating product catalog, shopping cart, order processing, and payment services into independent spaces.
- Social Media Networks: Isolating features like user profiles, news feeds, messaging, and search into distinct spaces.
- Financial Trading Systems: Decoupling order management, risk assessment, and execution services for improved performance and reliability.
- IoT Platforms: Handling data ingestion, device management, and analytics as separate, scalable spaces.
Examples
-
Netflix: Netflix heavily utilizes a space-based architecture. Different aspects of the streaming service, such as user authentication, recommendation engines, video encoding, and content delivery, are all implemented as independent microservices (spaces). This allows Netflix to scale individual components based on demand and deploy updates without disrupting the entire platform.
-
Amazon Web Services (AWS): AWS itself is a prime example. Each AWS service (e.g., S3, EC2, Lambda) operates as a largely independent space with its own API, data storage, and scaling mechanisms. The services interact through defined interfaces and event-driven communication, enabling a highly scalable and resilient cloud platform.
-
Spotify: Spotify’s backend is built on a space-based architecture, dividing functionality into areas like music catalog, user accounts, playlist management, and recommendation algorithms. This allows for independent scaling and development of each feature, supporting millions of users and a vast music library.
Specimens
15 implementationsThe Space-Based Architecture pattern decouples an application into independent, self-contained “spaces” that communicate via messages. Each space manages its own state and logic, reducing global state and improving modularity. This implementation uses Dart’s StreamController to create these spaces and message passing. Each space exposes a stream for receiving commands and a stream for emitting events. The main function orchestrates interactions between spaces by sending commands and listening for events. This approach aligns with Dart’s asynchronous programming model and promotes a reactive, event-driven architecture, making it well-suited for complex applications.
import 'dart:async';
// Define a Space (component)
class Space {
final StreamController<String> _commandController = StreamController<String>();
final StreamController<String> _eventController = StreamController<String>();
Stream<String> get commands => _commandController.stream;
Stream<String> get events => _eventController.stream;
void handleCommand(String command) {
print('Space received command: $command');
// Simulate processing
Future.delayed(Duration(milliseconds: 500), () {
_eventController.sink.add('Processed: $command');
});
}
void close() {
_commandController.close();
_eventController.close();
}
}
void main() {
final space1 = Space();
final space2 = Space();
// Listen for events from space1
space1.events.listen((event) => print('Main received event from Space 1: $event'));
// Listen for events from space2
space2.events.listen((event) => print('Main received event from Space 2: $event'));
// Send commands to spaces
space1.commands.sink.add('Command A');
space2.commands.sink.add('Command B');
space1.commands.sink.add('Command C');
// Allow time for processing and event emission
Future.delayed(Duration(seconds: 2), () {
space1.close();
space2.close();
print('Spaces closed.');
});
}
The Space-Based Architecture pattern decouples components of a system by using a message-based communication system, often referred to as a “space.” Components interact by sending and receiving messages without direct knowledge of each other. This promotes scalability, fault tolerance, and flexibility. In Scala, Akka’s Actors provide a natural implementation of this pattern. Each actor represents a component and communicates via asynchronous messages. This example demonstrates a simple order processing system with an OrderService and a PaymentService communicating through messages. The use of immutable data and pattern matching on messages is idiomatic Scala and enhances the clarity and safety of the interaction.
import akka.actor.{Actor, ActorSystem, Props}
// Messages
sealed trait OrderEvent
case class CreateOrder(orderId: String, amount: Double) extends OrderEvent
case object OrderCreated extends OrderEvent
case object PaymentProcessed extends OrderEvent
case class PaymentFailed(orderId: String, reason: String) extends OrderEvent
// Order Service Actor
class OrderService extends Actor {
override def receive: Receive = {
case CreateOrder(orderId, amount) =>
println(s"Order Service: Received CreateOrder for $orderId, amount $amount")
// Simulate order creation
sender() ! OrderCreated
case OrderCreated =>
println("Order Service: Order created. Requesting Payment.")
context.actorFor("PaymentService") ! CreateOrder(orderId = "123", amount = 100.0) // Hardcoded for simplicity
case PaymentProcessed =>
println("Order Service: Payment processed successfully.")
case PaymentFailed(orderId, reason) =>
println(s"Order Service: Payment failed for $orderId, reason: $reason")
}
}
// Payment Service Actor
class PaymentService extends Actor {
override def receive: Receive = {
case CreateOrder(orderId, amount) =>
println(s"Payment Service: Received CreateOrder for $orderId, amount $amount")
// Simulate payment processing
if (amount > 50) {
sender() ! PaymentProcessed
} else {
sender() ! PaymentFailed(orderId, "Amount too low")
}
}
}
// Main Application
object SpaceBasedArchitecture extends App {
val system = ActorSystem("SpaceBasedSystem")
val orderService = system.actorOf(Props[OrderService], "OrderService")
val paymentService = system.actorOf(Props[PaymentService], "PaymentService")
orderService ! CreateOrder("456", 75.0)
Thread.sleep(1000) // Allow messages to be processed
system.terminate()
}
The Space-Based Architecture pattern organizes code into loosely coupled, independent “spaces” or modules, each responsible for a specific aspect of the application. These spaces communicate through well-defined interfaces, minimizing dependencies and promoting modularity. This implementation uses PHP namespaces to define these spaces. Each space (e.g., User, Product, Order) contains related classes. A central “kernel” or “bootstrap” file handles dependency injection and initial setup, allowing spaces to be used independently or composed into larger applications. This approach aligns with PHP’s namespace feature and encourages a more organized, maintainable codebase, especially for larger projects.
<?php
namespace App\User;
interface UserRepository {
public function getUser(int $id): ?User;
}
class User {
private int $id;
private string $name;
public function __construct(int $id, string $name) {
$this->id = $id;
$this->name = $name;
}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
}
class InMemoryUserRepository implements UserRepository {
private array $users = [
1 => new User(1, "Alice"),
2 => new User(2, "Bob"),
];
public function getUser(int $id): ?User {
return $this->users[$id] ?? null;
}
}
namespace App\Product;
class Product {
private int $id;
private string $name;
private float $price;
public function __construct(int $id, string $name, float $price) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
}
public function getId(): int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function getPrice(): float {
return $this->price;
}
}
// Kernel/Bootstrap - minimal example
require_once __DIR__ . '/../vendor/autoload.php'; // Assuming Composer autoloader
function bootstrap(): void {
// Dependency Injection - could be more sophisticated
$userRepository = new InMemoryUserRepository();
// Spaces can now use $userRepository
}
bootstrap();
// Example Usage (outside the spaces, relying on bootstrap)
$user = $userRepository->getUser(1);
if ($user) {
echo "User: " . $user->getName() . "\n";
}
$product = new Product(101, "Example Product", 19.99);
echo "Product: " . $product->getName() . "\n";
?>
The Space-Based Architecture pattern organizes code into loosely coupled, independent modules (“spaces”) that communicate via a central “space” or message bus. This promotes modularity and allows components to be added, removed, or modified without impacting others. Our Ruby implementation uses a simple EventBus class to act as the central space. Modules register for and publish events to the bus. This leverages Ruby’s flexible object model and emphasizes the “message passing” paradigm. The use of observer pattern for the modules’ lifecycle makes it idiomatic for Ruby’s event-driven nature.
# event_bus.rb
class EventBus
def initialize
@observers = {}
end
def subscribe(event_name, observer)
@observers[event_name] ||= []
@observers[event_name] << observer
end
def publish(event_name, data)
@observers[event_name]&.each { |observer| observer.update(data) }
end
end
# module_a.rb
class ModuleA
def initialize(event_bus)
@event_bus = event_bus
@event_bus.subscribe("data_received", self)
end
def update(data)
puts "Module A received data: #{data}"
end
end
# module_b.rb
class ModuleB
def initialize(event_bus)
@event_bus = event_bus
end
def send_data(data)
@event_bus.publish("data_received", data)
end
end
# main.rb
require_relative 'event_bus'
require_relative 'module_a'
require_relative 'module_b'
event_bus = EventBus.new
module_a = ModuleA.new(event_bus)
module_b = ModuleB.new(event_bus)
module_b.send_data("Hello from Module B!")
The Space-Based Architecture pattern decouples components of an application by using a central “space” (often a dictionary or similar data structure) to store and retrieve data. Components communicate by publishing and subscribing to changes within this space. This avoids direct dependencies and promotes flexibility.
This Swift implementation uses a Space class holding a [String: Any] dictionary. Components register for updates on specific keys using closures. When a value associated with a key is updated, all registered closures are executed. This leverages Swift’s first-class functions and closures for a concise and type-safe approach. The use of a class encapsulates the shared state and update mechanism, aligning with Swift’s object-oriented capabilities. The Any type allows for storing diverse data types, though in a production system, more specific types would be preferred for better safety.
class Space {
private var data: [String: Any] = [:]
private var observers: [String: [() -> Void]] = [:]
func subscribe(key: String, observer: @escaping () -> Void) {
observers[key, default: []].append(observer)
}
func unsubscribe(key: String, observer: @escaping () -> Void) {
observers[key]?.removeAll { $0 === observer }
}
func publish(_ key: String, _ value: Any) {
data[key] = value
observers[key]?.forEach { $0() }
}
func observe<T>(key: String) -> T? {
return data[key] as? T
}
}
// Example Usage
class ComponentA {
private let space: Space
init(space: Space) {
self.space = space
space.subscribe(key: "message") {
print("Component A received update: \(self.space.observe(key: "message") ?? "No message")")
}
}
func sendMessage(message: String) {
space.publish("message", message)
}
}
class ComponentB {
private let space: Space
init(space: Space) {
self.space = space
space.subscribe(key: "message") {
print("Component B received update: \(self.space.observe(key: "message") ?? "No message")")
}
}
}
let space = Space()
let componentA = ComponentA(space: space)
let componentB = ComponentB(space: space)
componentA.sendMessage(message: "Hello from Component A!")
componentB.sendMessage(message: "Another message!")
The Space-Based Architecture pattern decouples components of an application by representing them as “spaces” that contain data and operations. Components communicate via message passing, avoiding direct dependencies. This promotes modularity, testability, and scalability. The Kotlin implementation uses data classes to represent messages and functions within each space. A central “message bus” (here, a simple list) facilitates communication. This approach leverages Kotlin’s conciseness for data representation and functional programming style for message handling, fitting its idiomatic approach to building loosely coupled systems.
// Space-Based Architecture in Kotlin
// Define message types
data class AddData(val data: String)
data class GetDataRequest
data class GetDataResponse(val data: String)
// Spaces - encapsulate data and operations
class DataSpace {
private var data: String = ""
fun handleMessage(message: Any) {
when (message) {
is AddData -> data = message.data
is GetDataRequest -> {
val response = GetDataResponse(data)
messageBus.add(response) // Publish response
}
}
}
}
class ProcessingSpace {
fun handleMessage(message: Any) {
if (message is GetDataResponse) {
println("Processing Space received data: ${message.data}")
}
}
}
// Message Bus - central communication point
val messageBus = mutableListOf<Any>()
fun main() {
val dataSpace = DataSpace()
val processingSpace = ProcessingSpace()
// Simulate message flow
dataSpace.handleMessage(AddData("Hello, Space-Based Architecture!"))
dataSpace.handleMessage(GetDataRequest())
// Process messages from the bus
messageBus.forEach { message ->
processingSpace.handleMessage(message)
}
messageBus.clear() // Clear the bus after processing
}
The Space-Based Architecture pattern decouples components of a system by using a message bus (or “space”) as the central communication hub. Components, known as “agents,” publish and subscribe to messages on this bus without direct knowledge of each other. This promotes loose coupling, scalability, and flexibility.
This Rust implementation uses the crossbeam-channel crate to create a multi-producer, multi-consumer channel acting as the message bus. Agents are represented by structs that hold a sender to publish messages. A simple Message enum defines the types of messages that can be sent. The main function creates agents and a receiver thread to process messages. This approach is idiomatic Rust due to its emphasis on ownership, message passing concurrency, and the use of crates for specific functionality. The crossbeam-channel provides a safe and efficient way to handle concurrent communication.
use crossbeam_channel::{unbounded, Receiver, Sender};
use std::thread;
// Define the messages that can be sent on the bus
#[derive(Debug)]
enum Message {
Data(i32),
Shutdown,
}
// Represents an agent that can publish messages
struct Agent {
sender: Sender<Message>,
}
impl Agent {
fn new(sender: Sender<Message>) -> Self {
Agent { sender }
}
fn send_data(&self, data: i32) {
self.sender.send(Message::Data(data)).unwrap();
}
fn send_shutdown(&self) {
self.sender.send(Message::Shutdown).unwrap();
}
}
fn main() {
let (sender, receiver) = unbounded();
// Create agents
let agent1 = Agent::new(sender.clone());
let agent2 = Agent::new(sender.clone());
// Spawn a receiver thread to process messages
let receiver_thread = thread::spawn(move || {
for message in receiver {
match message {
Message::Data(data) => println!("Received data: {}", data),
Message::Shutdown => {
println!("Shutting down receiver...");
break;
}
}
}
});
// Agents send messages
agent1.send_data(10);
agent2.send_data(20);
agent1.send_data(30);
// Signal shutdown
agent1.send_shutdown();
agent2.send_shutdown(); // Important to send shutdown from all agents.
// Wait for the receiver thread to finish
receiver_thread.join().unwrap();
println!("All agents finished.");
}
The Space-Based Architecture pattern structures an application as a collection of independent, loosely coupled services (spaces) that communicate via well-defined interfaces. Each space encapsulates specific business functionality and manages its own data. This promotes modularity, scalability, and independent deployment.
The Go code demonstrates this by defining interfaces for communication between spaces (e.g., OrderService, PaymentService). Concrete implementations (BasicOrderService, DummyPaymentService) represent individual spaces. The App struct orchestrates interactions between these spaces, adhering to dependency injection principles. Go’s interfaces and emphasis on composition make it well-suited for this pattern, allowing for flexible service integration and testability. The use of structs and methods aligns with Go’s preferred style for structuring applications.
// spaces.go
package main
import "fmt"
// Define interfaces for communication between spaces
type OrderService interface {
CreateOrder(orderData map[string]interface{}) (string, error)
GetOrder(orderID string) (map[string]interface{}, error)
}
type PaymentService interface {
ProcessPayment(orderID string, amount float64) error
}
// Concrete implementations of spaces
type BasicOrderService struct {
orders map[string]map[string]interface{}
}
func (o *BasicOrderService) CreateOrder(orderData map[string]interface{}) (string, error) {
orderID := fmt.Sprintf("order-%d", len(o.orders)+1)
o.orders[orderID] = orderData
return orderID, nil
}
func (o *BasicOrderService) GetOrder(orderID string) (map[string]interface{}, error) {
if order, ok := o.orders[orderID]; ok {
return order, nil
}
return nil, fmt.Errorf("order not found")
}
type DummyPaymentService struct{}
func (d *DummyPaymentService) ProcessPayment(orderID string, amount float64) error {
fmt.Printf("Processing payment for order %s, amount: %.2f\n", orderID, amount)
return nil
}
// Application orchestrator
type App struct {
orderService OrderService
paymentService PaymentService
}
func NewApp(orderService OrderService, paymentService PaymentService) *App {
return &App{
orderService: orderService,
paymentService: paymentService,
}
}
func (a *App) HandleOrder(orderData map[string]interface{}) (string, error) {
orderID, err := a.orderService.CreateOrder(orderData)
if err != nil {
return "", err
}
amount, ok := orderData["amount"].(float64)
if !ok {
return "", fmt.Errorf("invalid amount in order data")
}
err = a.paymentService.ProcessPayment(orderID, amount)
if err != nil {
return "", err
}
return orderID, nil
}
func main() {
// Initialize spaces
orderService := &BasicOrderService{orders: make(map[string]map[string]interface{})}
paymentService := &DummyPaymentService{}
// Create application
app := NewApp(orderService, paymentService)
// Example usage
orderData := map[string]interface{}{
"customer": "Alice",
"items": []string{"Book", "Pen"},
"amount": 25.0,
}
orderID, err := app.HandleOrder(orderData)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Order created with ID:", orderID)
}
}
The Space-Based Architecture (SBA) pattern decouples application logic from the execution environment by defining a set of independent “spaces” that contain the necessary resources (data, configuration, etc.) for a specific task. A “space handler” manages these spaces, providing access to the required resources. This promotes modularity, testability, and allows for easy swapping of implementations.
The C implementation uses opaque pointers to represent spaces and a function pointer table (the space_handler) to encapsulate space-specific operations. This avoids exposing the underlying space data structure directly. The create_space, get_space_data, and destroy_space functions demonstrate the core SBA principles. Using function pointers is a common C idiom for achieving polymorphism and decoupling. The structure-based approach to defining the space handler aligns with C’s preference for explicit memory management and data organization.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Opaque space pointer
typedef struct space_t *space_ptr;
// Space data (example)
typedef struct {
int value;
char *message;
} space_data_t;
// Function pointer type for space handler
typedef struct {
space_ptr (*create)(int initial_value, const char *initial_message);
space_data_t* (*get_data)(space_ptr space);
void (*destroy)(space_ptr space);
} space_handler_t;
// Concrete Space Implementation
struct space_t {
space_data_t data;
};
// Space Handler Functions
space_ptr create_space(int initial_value, const char *initial_message) {
space_ptr space = (space_ptr)malloc(sizeof(struct space_t));
if (space == NULL) {
perror("Failed to allocate space");
return NULL;
}
space->data.value = initial_value;
space->data.message = strdup(initial_message); // Duplicate the string
if (space->data.message == NULL) {
perror("Failed to duplicate message");
free(space);
return NULL;
}
return space;
}
space_data_t* get_space_data(space_ptr space) {
if (space == NULL) {
return NULL;
}
return &(space->data);
}
void destroy_space(space_ptr space) {
if (space != NULL) {
free(space->data.message);
free(space);
}
}
// Example Usage
int main() {
space_handler_t handler = {
.create = create_space,
.get_data = get_space_data,
.destroy = destroy_space
};
space_ptr my_space = handler.create(42, "Hello, Space!");
space_data_t* data = handler.get_data(my_space);
if (data != NULL) {
printf("Value: %d\n", data->value);
printf("Message: %s\n", data->message);
}
handler.destroy(my_space);
return 0;
}
The Space-Based Architecture pattern decouples the data structures used by a system from the algorithms that operate on that data. Instead of tightly coupling data and methods within classes (like traditional OOP), it focuses on representing data as a collection of “spaces” – contiguous memory blocks – and providing separate functions to manipulate these spaces. This promotes flexibility, allows for easy addition of new algorithms without modifying data structures, and can improve performance through data locality.
The C++ example uses std::vector to represent the data spaces. Separate functions (process_data, calculate_average) operate on these vectors, taking them as input. This avoids class encapsulation of the data and algorithms, favoring a more functional approach where data is passed to functions for processing. Using vectors is idiomatic C++ for dynamic arrays and provides efficient contiguous storage, aligning with the pattern’s emphasis on data locality.
#include <iostream>
#include <vector>
#include <numeric> // For std::accumulate
// Data spaces (represented as vectors)
std::vector<int> data_space_1;
std::vector<int> data_space_2;
// Algorithm 1: Process data in a space
void process_data(std::vector<int>& space) {
for (int& value : space) {
value *= 2; // Example processing: double each value
}
}
// Algorithm 2: Calculate the average of a space
double calculate_average(const std::vector<int>& space) {
if (space.empty()) {
return 0.0;
}
double sum = std::accumulate(space.begin(), space.end(), 0.0);
return sum / space.size();
}
int main() {
// Initialize data spaces
data_space_1 = {1, 2, 3, 4, 5};
data_space_2 = {6, 7, 8, 9, 10};
// Process data in space 1
process_data(data_space_1);
// Calculate the average of space 2
double average_space_2 = calculate_average(data_space_2);
// Output results
std::cout << "Data Space 1 (processed): ";
for (int value : data_space_1) {
std::cout << value << " ";
}
std::cout << std::endl;
std::cout << "Average of Data Space 2: " << average_space_2 << std::endl;
return 0;
}
The Space-Based Architecture pattern decouples application logic by using a central “space” (often a dictionary or similar data structure) to store application state and a set of independent “agents” that react to changes in that space. Agents subscribe to specific state changes and perform actions accordingly. This avoids direct dependencies between components, promoting flexibility and testability.
The C# example uses a Dictionary as the space, holding string keys representing events and object values representing event data. Agents are implemented as classes subscribing to events triggered when the space is updated. The use of events and delegates is idiomatic C# for loosely coupled communication. The Space class encapsulates the state and provides a controlled way to update it, triggering agent reactions.
using System;
using System.Collections.Generic;
// The "Space" - holds application state
public class Space
{
private readonly Dictionary<string, object> _state = new Dictionary<string, object>();
public event EventHandler<StateChangedEventArgs> StateChanged;
public object GetState(string key)
{
if (_state.ContainsKey(key))
{
return _state[key];
}
return null;
}
public void SetState(string key, object value)
{
_state[key] = value;
OnStateChanged(key, value);
}
protected virtual void OnStateChanged(string key, object value)
{
StateChanged?.Invoke(this, new StateChangedEventArgs(key, value));
}
}
// Event arguments for state changes
public class StateChangedEventArgs : EventArgs
{
public string Key { get; }
public object Value { get; }
public StateChangedEventArgs(string key, object value)
{
Key = key;
Value = value;
}
}
// An "Agent" - reacts to state changes
public class OrderAgent
{
private readonly Space _space;
public OrderAgent(Space space)
{
_space = space;
_space.StateChanged += OnSpaceStateChanged;
}
private void OnSpaceStateChanged(object sender, StateChangedEventArgs e)
{
if (e.Key == "newOrder")
{
var order = (string)e.Value;
Console.WriteLine($"Order Agent: Processing new order - {order}");
// Perform order processing logic here
}
}
}
// Another "Agent"
public class InventoryAgent
{
private readonly Space _space;
public InventoryAgent(Space space)
{
_space = space;
_space.StateChanged += OnSpaceStateChanged;
}
private void OnSpaceStateChanged(object sender, StateChangedEventArgs e)
{
if (e.Key == "newOrder")
{
var order = (string)e.Value;
Console.WriteLine($"Inventory Agent: Updating inventory for order - {order}");
// Perform inventory update logic here
}
}
}
// Example Usage
public class Program
{
public static void Main(string[] args)
{
var space = new Space();
var orderAgent = new OrderAgent(space);
var inventoryAgent = new InventoryAgent(space);
space.SetState("newOrder", "Order #123 - Widget A x 2");
space.SetState("newOrder", "Order #456 - Gadget B x 1");
}
}
The Space-Based Architecture pattern organizes code into independent, self-contained “spaces” that communicate via well-defined interfaces (often events or messages). This promotes loose coupling, making the system more modular, testable, and easier to evolve. Each space encapsulates specific functionality and data, minimizing dependencies on other parts of the system.
This TypeScript implementation uses a simple event emitter pattern to define the spaces and their communication. OrderService represents one space, emitting ‘order_placed’ events. InventoryService subscribes to these events to update stock. NotificationService also subscribes to handle notifications. TypeScript’s type system and class-based structure naturally support encapsulation and interface definition, making it a good fit for this pattern. The use of events avoids direct dependencies between services.
// event-emitter.ts
class EventEmitter {
listeners: { [event: string]: Function[] } = {};
on(event: string, listener: Function) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(listener);
}
emit(event: string, data: any) {
if (this.listeners[event]) {
this.listeners[event].forEach(listener => listener(data));
}
}
}
// order.service.ts
class OrderService {
private emitter = new EventEmitter();
placeOrder(order: { items: string[], quantity: number }) {
console.log(`Order placed: ${JSON.stringify(order)}`);
this.emitter.emit('order_placed', order);
}
onOrderPlaced(listener: (order: { items: string[], quantity: number }) => void) {
this.emitter.on('order_placed', listener);
}
}
// inventory.service.ts
class InventoryService {
private stock: { [item: string]: number } = {};
constructor() {
this.stock = { 'widget': 100, 'gadget': 50 };
}
handleOrderPlaced(order: { items: string[], quantity: number }) {
order.items.forEach(item => {
if (this.stock[item]) {
this.stock[item] -= order.quantity;
console.log(`Inventory updated: ${item} - remaining: ${this.stock[item]}`);
} else {
console.warn(`Item not found in inventory: ${item}`);
}
});
}
}
// notification.service.ts
class NotificationService {
handleOrderPlaced(order: { items: string[], quantity: number }) {
console.log(`Sending notification for order: ${JSON.stringify(order)}`);
// Simulate sending a notification (e.g., email, SMS)
}
}
// app.ts
const orderService = new OrderService();
const inventoryService = new InventoryService();
const notificationService = new NotificationService();
orderService.onOrderPlaced(inventoryService.handleOrderPlaced.bind(inventoryService));
orderService.onOrderPlaced(notificationService.handleOrderPlaced.bind(notificationService));
orderService.placeOrder({ items: ['widget'], quantity: 5 });
orderService.placeOrder({ items: ['gadget'], quantity: 10 });
The Space-Based Architecture pattern organizes code into independent “spaces” (often modules or namespaces) that communicate via well-defined interfaces. This promotes modularity, reduces coupling, and allows for easier testing and maintenance. Each space encapsulates specific functionality and data, exposing only what’s necessary for interaction.
This JavaScript example uses ES modules to create two spaces: calculator and display. The calculator space handles the core calculation logic, while the display space focuses on presenting the results. They interact through exported functions and imported dependencies. This approach is idiomatic JavaScript because modules are the standard way to achieve encapsulation and manage dependencies in modern JavaScript. The separation of concerns and explicit interface definitions align with JavaScript’s flexible, yet maintainable, design principles.
// calculator.js
let result = 0;
export function add(num) {
result += num;
return result;
}
export function subtract(num) {
result -= num;
return result;
}
export function getResult() {
return result;
}
// display.js
import { add, subtract, getResult } from './calculator.js';
export function updateDisplay() {
const displayElement = document.getElementById('display');
if (displayElement) {
displayElement.textContent = getResult();
} else {
console.log("Display element not found.");
}
}
export function handleOperation(operation, number) {
if (operation === 'add') {
add(number);
} else if (operation === 'subtract') {
subtract(number);
}
updateDisplay();
}
// main.js
import { handleOperation } from './display.js';
document.addEventListener('DOMContentLoaded', () => {
const addButton = document.getElementById('addButton');
const subtractButton = document.getElementById('subtractButton');
if (addButton) {
addButton.addEventListener('click', () => handleOperation('add', 5));
}
if (subtractButton) {
subtractButton.addEventListener('click', () => handleOperation('subtract', 3));
}
});
// index.html (minimal example for running)
// <!DOCTYPE html>
// <html>
// <head>
// <title>Space-Based Architecture Example</title>
// </head>
// <body>
// <button id="addButton">Add 5</button>
// <button id="subtractButton">Subtract 3</button>
// <div id="display">0</div>
// <script type="module" src="main.js"></script>
// </body>
// </html>
The Space-Based Architecture pattern structures an application as a collection of independent, loosely coupled “spaces” that each handle a specific aspect of the overall functionality. These spaces communicate via well-defined interfaces, often using message passing or event-driven mechanisms. This promotes modularity, testability, and scalability.
The Python example below defines three spaces: AuthenticationSpace, UserProfileSpace, and DataProcessingSpace. Each space encapsulates its logic and exposes methods for interaction. A simple MessageBus facilitates communication between them. This approach aligns with Python’s emphasis on modularity and readability, leveraging classes to represent spaces and functions for their internal operations. The message bus is a common pattern in Python for decoupling components.
# message_bus.py
class MessageBus:
def __init__(self):
self.handlers = {}
def register_handler(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event_type, data):
if event_type in self.handlers:
for handler in self.handlers[event_type]:
handler(data)
# authentication_space.py
class AuthenticationSpace:
def __init__(self, message_bus):
self.message_bus = message_bus
self.message_bus.register_handler("user_login_request", self.handle_login)
def handle_login(self, user_data):
# Simulate authentication logic
if user_data["username"] == "user" and user_data["password"] == "password":
self.message_bus.publish("user_login_success", {"user_id": 123})
else:
self.message_bus.publish("user_login_failure", {"error": "Invalid credentials"})
def request_login(self, username, password):
self.message_bus.publish("user_login_request", {"username": username, "password": password})
# user_profile_space.py
class UserProfileSpace:
def __init__(self, message_bus):
self.message_bus = message_bus
self.message_bus.register_handler("user_login_success", self.load_profile)
self.user_profile = None
def load_profile(self, user_id):
# Simulate loading user profile
self.user_profile = {"id": user_id, "name": "Example User"}
print(f"User profile loaded: {self.user_profile}")
def get_profile(self):
return self.user_profile
# data_processing_space.py
class DataProcessingSpace:
def __init__(self, message_bus):
self.message_bus = message_bus
self.message_bus.register_handler("user_login_success", self.start_processing)
def start_processing(self, user_id):
# Simulate data processing
print(f"Starting data processing for user {user_id}")
# main.py
if __name__ == "__main__":
bus = MessageBus()
auth_space = AuthenticationSpace(bus)
profile_space = UserProfileSpace(bus)
data_space = DataProcessingSpace(bus)
auth_space.request_login("user", "password")
auth_space.request_login("wrong_user", "wrong_password")
The Space-Based Architecture pattern decouples application logic by using a shared, immutable context (the “space”) to pass data between components. Instead of direct dependencies, components register to receive notifications when data they’re interested in changes within the space. This promotes loose coupling and allows components to be added or removed without impacting others.
This Java implementation uses a simple Space class holding a Map of data. Components register for specific keys and receive updates via a callback interface (Listener). The Space manages the Listeners and notifies them when data associated with their key changes. This is broadly analogous to a pub-sub system, but centralized within the ‘space’. Using interfaces for the component and listener adheres to Java’s preference for abstraction, while the Map provides efficient data lookups, enabling lightweight communication suitable for distributed systems. The immutability of the data within the space is enforced by returning copies of the data to the listeners.
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
interface Listener {
void onDataChanged(String key, Object newValue);
}
interface Component {
String[] getInterestedInKeys();
void setData(String key, Object value);
}
class Space {
private final Map<String, Object> data = new HashMap<>();
private final List<Listener> listeners = new ArrayList<>();
public void register(Listener listener, String... keys) {
listeners.add(listener);
for (String key : keys) {
notifyListeners(key, data.getOrDefault(key, null)); // Initial notification
}
}
public void unregister(Listener listener) {
listeners.remove(listener);
}
public void put(String key, Object value) {
if (!data.containsKey(key) || !data.get(key).equals(value)) {
data.put(key, value);
notifyListeners(key, value);
}
}
private void notifyListeners(String key, Object value) {
for (Listener listener : listeners) {
listener.onDataChanged(key, value);
}
}
public Object getData(String key) {
return data.get(key); //Return a copy if immutability is critical
}
}
class MyComponent implements Component {
private final Space space;
private final String componentName;
public MyComponent(Space space, String componentName) {
this.space = space;
this.componentName = componentName;
space.register(this::handleDataChange, "temperature", "humidity");
}
@Override
public String[] getInterestedInKeys() {
return new String[]{"temperature", "humidity"};
}
@Override
public void setData(String key, Object value) {
space.put(key, value);
}
public void handleDataChange(String key, Object newValue) {
System.out.println(componentName + " received update for " + key + ": " + newValue);
}
}
public class SpaceBasedArchitecture {
public static void main(String[] args) {
Space space = new Space();
MyComponent component1 = new MyComponent(space, "Component 1");
MyComponent component2 = new MyComponent(space, "Component 2");
space.put("temperature", 25);
space.put("humidity", 60);
space.put("temperature", 28);
}
}