architecture – Containers or Serverless? What Affects Your Architectural Decision?

I am curious how folks in the community reason about “containers” vs. “serverless” and different takes.

I recently came across the Dapr project: https://dapr.io/ which I think is a conceptually very interesting approach to system architecture that allows building a portable application that can be plugged into AWS, Azure, GCP, etc.

There is a reference eShop project on github: https://github.com/dotnet-architecture/eShopOnDapr

I have worked with Azure Functions, AWS Lambda, and AWS AppSync but not very heavily with containerized workloads.

Looking at the eShop reference project, I cannot get over how complex the application is compared to what it would take to build the same functionality with the same general application level characteristics (decoupled, event driven, service oriented) using Lambda, AppSync, or Functions. And this got me wondering where teams find this level of complexity worthwhile.

As I understand it, there are a handful of benefits gained from the containerized approach:

  1. Long running application logic. Most serverless frameworks have short timeout quotas so they are not suited for long running processes.
  2. Memory or CPU intensive workloads. With serverless, you have less control over resource intensive workloads. So if you have a workload that requires more control over memory or CPU, containers are a better alternative
  3. Portability. Containerized applications are more portable between different platforms. On-premise, AWS, Azure, or Google — you can deploy your containerized workloads easily between these platforms. It is possible with serverless as well IMO by moving most of your business logic into application libraries (e.g. a layer in Lambda) and using the serverless interface primarily as a bridge to I/O.
  4. (3a) Flexibility. Extending portability, a solution like Dapr would allow the underlying platform components to be replaced. Developing natively for Azure Functions requires using Functions specific bindings to event sources such as Service Bus or Event Grid whereas Dapr would allow the application to plug AWS SNS as an event source on AWS and Service Bus on Azure. In some mission critical scenarios, this may even be THE deciding factor as it allows a multi-cloud deployment (though you have a different challenge of synchronizing data between the clouds) that could theoretically stay up even if one of the major providers goes down.
  5. Legacy apps. Some legacy apps may be easier to “lift-and-shift” into containers whereas moving into Functions or Lambda is a re-write. For example, applications which are written as daemons. Some legacy apps would not fit into a serverless paradigm at all and would require mapping to another platform specific service.
  6. Persistent workloads. Serverless benefits from burst type workloads since there is no cost when the system is not operating. For example, enterprise line of business systems which tend to have predictable spikes in the start of a workday and early afternoon. At night, when there are no users, there is no cost component for the compute. On the other hand, some types of systems which see continuous throughput may be more cost effective with containers.
  7. Ops can be simplified in some cases. By deploying the application components and dependencies (e.g. Redis) as containers, it can be easier to to manage than building complex IAC configuration using ARM templates, CF templates, etc.
  8. Programming language. Functions will have limited support for programming languages so if you are using a less commonly deployed language, you would benefit from the container runtime.

I would love to hear from folks who are working with containerized workloads and who have worked with serverless to get some different angles and other perspectives I’m missing.

It seems to me that in the majority of cases, the application code and deployment model is considerably more concise and easier to reason about with serverless than with containerized workloads for most web application APIs if one is willing to accept the tradeoffs (e.g. lack of platform portability).

programming practices – If Entities, in the Clean Architecture, are enterprise wide rules how different applications consume them?

In this post, Uncle Bob writes:

Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.

If I understood correctly, entities are business rules that apply to the whole company. It’s what defines the business. And the rules that the entities embody can be executed by one or many applications that make the whole enterprise system.

I’m thinking in terms of a company that has dozens of microservices and multiple web-apps, all in different programming languages. How is it possible to centralize enterprise wide business rules in such scenario?

architecture – Order Management Microservice design pattern

One approach that works very well with transaction based interactions (like order management, supply chain management, stock trading, etc) is Event Sourcing. With an Event Sourced architecture, your event stream is your model. You then can project that stream of events into the forms that better meet your query and display needs.

In this approach, every order will have its own Event Stream (list of events from the beginning of the order to its completion).

This architecture works well with Command Query Responsibility Separation (CQRS) where Commands perform all the validations and preparatory work needed for your business process, but the end result is appending a message to the Event Stream for your Order.

Naturally, the concept of an Event Store has come up to derive the need of persistence. The Event Store will store the chronological stream of events for each Event Stream so they can be replayed.

If you combine the two you have a very powerful architecture. You can have separate microservices for the business logic side of things and the display side of things. The business logic is done in Commands, and writes events to the appropriate Event Stream into the Event Store. The view side of things reads from the Event Store the events not processed in the Event Stream, and Projects it into a database (RDBMS, graph, document, search) that is better suited for querying. That allows you to scale the writes and views separately if that’s needed.

If necessary, you can build new Command microservices for each of the new responsibilities. Those commands perform the validations and additional logic needed to make sure the process is done correctly, and the end result is an Event in the EventStream to record that the work has been done.

Critical aspects of Event Sourcing:

  • Events are in Past Tense, indicating work that has already been completed
  • Projections (in functional languages, a left fold) transform the Event Stream for display or query purposes
  • The Event Stream is your authoritative state

Distinction between Event Sourcing and Event Driving:

  • Event Driven architecture has events in Present Tense, and reflect commands that will be handled by another service
  • Event Sourced architecture has events in Past Tense, reflecting that the work has already been done (AKA facts)
  • Event Sourcing does not require sending events over a queue, a similar effect can be had by having other services query a shared event store

Pros:

  • Work can be distributed among many microservices that handle one function, allowing you to distribute logic appropriately
  • The Event Stream doubles as an authoritative audit log

Cons:

  • Event Versioning is something that you have to consider carefully
  • There can be conflicting events, requiring human review–particularly in a distributed environment

NOTE: CQRS is not necessary with Event Sourcing, but it fits naturally and works well with it. Many examples you find with Event Sourcing is coupled with CQRS.

architecture – Communication between two apps

The idea that App 2 makes a request back to App 1 is a common architecture to get web services to work together. Usually, this would work like this:

  • App 1 makes a request to App 2
    • the request contains an URL on App 1 to be called upon completion
  • App 2 responds to the requests, and begins background work
  • eventually, App 2 requests the URL provided by App 1’s initial request
    • this request can either contain necessary status data, or App 2 might have an API from which App 1 can retrieve necessary data

Such a design is e.g. common on many authorization or payment flows on the web, where multiple parties must work together. However, there’s a lot of back-and-forth here, which might be inefficient.

A completely different approach uses message queues.

  • App 1 publishes a message on a topic
    • this happens asynchronously, so there is no initial response by App 2
    • each message has an ID
  • App 2 is listening on that topic and will eventually process the message
  • App 2 publishes a new message with results
    • the results message will have a correlation-ID that matches the ID of the request
  • App 1 is listening for results and will eventually receive a results message with the desired correlation ID

Such message queue based architectures are especially interesting in high-throughput scenarios, or in an enterprise context where you want to be able to easily add more listeners for events, instead of HTTP-style 1:1 communication.

Celery is a Python tool that uses such message queues, but simplifies them. With Celery, Celery is the App 2 – you must start a celery worker server. This lets App 1 easily perform some function call as a background task. This is similar to using async functions or to starting a thread, but the Celery worker doesn’t have to run on the same server.

So if you just want asynchronous background tasks, Celery is a good fit. If you already have an App 2, Celery won’t help. Instead, you should implement one of the HTTP-request or message queue based approaches.

How does Hexagonal architecture solve N-tier’s service spagetti?

I’ve seen a couple of talks on Hexagonal architecture where they illustrate problems with N-tier architecture. Specifically, they show a spaghetti of service layer dependencies as the software grows. For example, the image below is from the Hexagonal Architecture with Grails talk: https://www.infoq.com/presentations/hexagonal-arch-grails/

hexagonal architecture with grails

I agree this is typically where N-tier apps wind up as they grow. But I don’t really understand how hexagonal arch avoids or improves this situation. The business logic will still have the same dependencies. Those dependencies exist because some business code (whether intermingled with framework/persistence code or not), needs to call some other business code. Whether it’s in a service layer or in something we call “core” seems irrelevant.

Somewhat related is the concept of number of layers. While the talks on hexagonal architecture say it’s a single layer architecture (or outside versus inside), it all comes down to perception or how it’s drawn in diagrams. In my view it’s still N-tier, except we can call the web tier and data layer “adapters”. Whether it’s represented as horizontal layers or vertical slices seems more a matter of illustration or code organization. But the latter is often dictated by our framework.

testing – Change architecture design to API to reduce coupling

I just don’t see an API reducing all those dependencies

Because it doesn’t. Uncle Bob is wrong there. Let me list some reasons for this:

The very first thing is, Uncle Bob doesn’t seem to think in terms of design trade-offs. It seems to be always in absolutes, like the UI must always be separated from the business, database and all other technologies must all be separated from each other, etc. Just take a look at Uncle Bob’s code to see how unmaintainable that is.

Architecture is heavily dependent on the requirements. That means you can’t make a trade-off “in general” without even looking at what that application is for.

Second, introducing an additional abstraction is not free. Putting a box in a diagram might be easy, but you’re bringing a lot of complexity on board. This is also not acknowledged.

Third, it doesn’t even decouple anything necessarily. Uncle Bob leaves out all the details about that one additional box. Like, is that API a data-oriented API, like Uncle Bob usually defines them? Because if it is, both sides have to know what that data is and how it works. That would mean that if the “services” change, the “users” have to change anyway, because of the changed semantics and/or protocol. What if the workflow changes, the meaning of some data elements, etc.?

He might mean, that you don’t exactly need to know which service you need to call, because you’re calling a central thing. That might help you in some situations, but that’s a pretty small part of the whole thing.

In general though, I agree with the conclusions. TDD will not get you a good design automatically, it is not a design practice. It is still all up to us.

architecture – Change achitecture design to API to reduce coupling

I just don’t see an API reducing all those dependencies

Because it doesn’t. Uncle Bob is wrong there. Let me list some reasons for this:

The very first thing is, Uncle Bob doesn’t seem to think in terms of design trade-offs. It seems to be always in absolutes, like the UI must always be separated from the business, database and all other technologies must all be separated from each other, etc. Just take a look at Uncle Bob’s code to see how unmaintainable that is.

Architecture is heavily dependent on the requirements. That means you can’t make a trade-off “in general” without even looking at what that application is for.

Second, introducing an additional abstraction is not free. Putting a box in a diagram might be easy, but you’re bringing a lot of complexity on board. This is also not acknowledged.

Third, it doesn’t even decouple anything necessarily. Uncle Bob leaves out all the details about that one additional box. Like, is that API a data-oriented API, like Uncle Bob usually defines them? Because it is, both sides have to know what that data is and how it works. That would mean that if the “services” change, the “users” have to change anyway, because of the changed semantics and/or protocol. What if the workflow changes, the meaning of some data elements, etc.?

He might mean, that you don’t exactly need to know which service you need to call, because you’re calling a central thing. That might help you in some situations, but that’s a pretty small part of the whole thing.

In general though, I agree with the conclusions. TDD will not get you a good design automatically, it is not a design practice. It is still all up to us.

javascript – How to build a scalable Architecture that must support multiple brands, flows and markets?

Looking to get some opinions/suggestions on how to manage or improve an application that supports multiple brands, markets and flows.

In this example I am using a form. There is one base form that can potentially be different for each flow & market. A telephone number may be required for one flow but not another or it might require a different format for each market.

A suggested approach using a slot technique:

const ExampleTopLevelComponent = ({ customSlots = {}, ...props }) => {
  const newSlots = {
    ...exampleSlot1(), // Field
    ...exampleSlot2(), // Field
    ...exampleSlot3(), // Field
    ...exampleSlot4(), // Field
  };

  const combinedSlots = { ...customSlots, ...newSlots };

  switch (condition) {
    case "BRAND":
      return <ExampleBrandSecondLevel customSlots={combinedSlots} {...props} />;
    default:
      return combinedSlots;
  }
};

const ExampleBrandSecondLevel = ({ customSlots, ...props }) => {
  const newSlots = {};
  const combinedSlots = { ...customSlots, ...newSlots };

  switch (flow) {
    case "FLOW_1":
      return <ComponentFlow1 customSlots={combinedSlots} {...props} />;
    ...
    ...
    default:
      return combinedSlots;
  }
};

const ComponentFlow1 = ({ customSlots = {}, ...props }) => {
  const newSlots = {};
  const combinedSlots = { ...customSlots, ...newSlots };

  switch (market) {
    case "MARKET_1":
      return <ExampleMarket1 customSlots={combinedSlots} {...props} />;
    ...
    ...
    default:
      return combinedSlots;
  }
};

const ExampleMarket1 = ({ customSlots = {} }) => {
  const newSlots = {
    ...exampleSlot1({ readOnly: true }), // Overwrite top level
    ...newSlot1(),
  };

  const combinedSlots = { ...customSlots, ...newSlots };

  return combinedSlots;
};

Final object for render would be the following when flow = ‘FLOW_1’ and market = ‘MARKET_1’.

const slots = {
  ...exampleSlot1({ readOnly: true }),
  ...exampleSlot2(),
  ...exampleSlot3(),
  ...exampleSlot4(),
  ...newSlot1(),
};

This does work, however it is causing a lot of duplicate code as I need to provide the same scaffolding for all components across the application. When the apps grows the overhead will be huge and difficult to manage.

I have thought about a schema approach but that might be difficult to read when there are 15+ slots with different customisations such as labels, onClicks etc. etc.

architecture – Some thoughts on the Repository pattern

There is more than one way to skin a cat.

Your question is asking how something is done, but it’s hard to provide an accurate answer when there are many possible different approaches. I’ve decided to pick the most commonly used one to answer with. Keep in mind that this is not the only possible approach.

Imagine we have an “IItemRepo” interface implemented by the Persistence mechanism.

  • Where this object will be instantiated?

Services (i.e. objects that aren’t DTOs) are generally instantiated by the DI container. You don’t actually have to do anything here, other than register the concrete type to the interface:

services.AddScoped<IItemRepository, MyItemRepository>();

In short, what this config line does is the same as telling the DI container:

Hey, whenever you are creating an object, and its constructor asks for an IItemRepository parameter, I want you to pass it an instance of MyItemRepository.

You effectively do this registration for each and every service in your codebase. Then, when you want to instantiate your top level service (e.g. web controller), the DI container is capable of providing each related service, all the way down to the bottom of the call stack, with all of the dependency that they need injection.

  • Who will have knowledge of this object?

Of the object, not type? That depends on how you configure your DI container. In the example above, the method name specifies the approach.

  • AddTransient means that all injected IItemRepository dependencies will always get a freshly generated MyItemRepository instance, it is never reused.
  • AddSingleton means that all injected IItemRepository dependencies will refer back to the same singular MyItemRepository instance.
  • AddScoped behaves just like AddSingleton, but on a smaller level. Instead of reusing it across the entire runtime, it reuses it in a smaller scope. For web based applications, that scope is defined as the incoming web request. For a given web request, the same service instance will be reused, but a different web request will get a different service instance. Because of that, AddScoped is the most commonly preferred option.

As to the “who has knowledge” part of the question, it’s essentially anyone who has a dependency on the given type.

  • How the information that it provides will end up be used by the Domain/Application layer? Factories, but how exactly?

This question is unclear. I suspect that you’re thinking of factories in terms of creating the services (in this case the repository instance).

The DI container I’ve been speaking of is a kind of factory. It’s just a really intelligent and customizable one, which is able to generate an instance of any registered type.

  • Will this object be passed around from layer to layer?

Yes and no. Yes, some of the things you observe are correct, but no, your conclusion does not follow.

Yes, it is possible for services from multiple layers to both have a dependency of the same type. Yes, when that dependency is not configured to be transient, that means that both of these services from multiple layers will receive the same instance of that dependency.
On a larger scope, yes, the DI container and its configuration must contain a whole lot of information on all of your layers, aggregated together. That is the top-level application’s responsibility (e.g. your Web project).

No, this does not count as “passing around data”. Object initialization is different from object usage, when dealing with dependency injection.
In any application, to some degree all of its classes are connected into a single web of dependency/references. If there were two completely separated webs in your codebase, you’d have two completely different applications.
When using dependency injection, that “web” is defined using the constructor parameters. Therefore, the constructors no longer conceptually count as the data highway, but rather as application architecture.

Aren’t DTO’s the only objects allowed this kind of behavior?

You’re inverting the logic here. It’s not that DTO’s are the only one allowed to cross the layer boundary. It’s that we call something a DTO when it crosses the layer boundary.

But similar as mentioned above, when using dependency injection, constructors are part of the architecture club in your codebase, which excludes their usage as being considered data-related, but rather architecture-related. And that’s a different ballgame with different rules.

architecture – Configuration of a staging environment vs production environment

For our software development process we used to set up 3 environments : integration , QA and master.

Recently it was decided to add a new staging environment that shall mirror the production environment for further testing. Our production environment uses kubernetes, SSL, Load balancing, and others components and configuration elements. Do I need to use all of them in the configuration of the staging environment as well?

Also, what other configuration differences should we keep in mind when setting up a staging vs production environments?

(I apologize for the question, but I am not experienced in the definition of system landscapes for software development processes).