The Agile Way to
Architect Web Apps
Doguhan Uluca
The Agile Way to
Architect Web Apps
Technical Fellow
Excella
Doguhan Uluca
What is a Line-of-Business App?
"set of critical computer applications perceived as vital to running an enterprise"
Source: Wikipedia
5W5H
- What do we develop?
- Why/when/where do we develop it?
- Who develops it?
- How do we develop it?
- Architecture
- API Design
- Web App Design
- Imperative vs Reactive
- Router-first architecture
What do we develop?
When/where/why do we develop?
Personal
Enterprise
- On your own time
- Passion-driven
- Educational
- Likely under engineered
- On company time
- Requirement-driven
- Likely over engineered
When/where/why do we develop?
Personal development
Enterprise development
Line-of-Business Apps
Personal
Enterprise
80% of useful apps
Personal
Enterprise
The Enterprise Niche
extremely complicated or large LOB apps, tools or products
Line-of-Business Apps
-
When we start coding, we really never know how much or how little our app will be used
-
Our apps can quickly become a LOB app
-
LOB apps are really important to businesses
So
How should we develop?
-
Must deal with demands of iterative and incremental delivery
-
Must achieve a constant forward flow of features
-
Must do all the cloud things
-
Be scalable
-
Serve dozens of screen and input types
-
Must do usability, accessibility
-
Can't have more than 8 people on the team
-
Must do backlog grooming
-
Make sure acceptance criteria is acceptable
However...
-
CS grads
-
Career switchers
-
Reluctant companies
-
Foot draggers
-
Lone wolves
-
Team players
-
Learners
-
Stack-Overflowers
Who develops it?
😰
- Size of our app
- Reason we are developing the app
- Skill level of developers
- Iterative and incremental delivery
- Constant forward flow of features
- All the cloud things
How?
The right architecture, tools and patterns/practices.
?
💡
Fundamental Architecture
&
Engineering Fundamentals
High Level Goals of Architecture
- High Cohesion
- Low coupling
N-tier Architecture
SOLID
DRY
Modern Web Architecture
- High cohesion
- Low coupling
- Asynchronous
- Non-blocking I/O
- Multi-threaded
- Parallel execution
- High availability
- Self-healing
- Auto-scaling
- RTO and RPO expressed in minutes/seconds
STATELESS
COMPOSABLE
REACTIVE
-
API DESIGN
-
WEB APP ARCHITECTURE
API design with REST
- REST is robust, but inflexible
- API documentation must be enabled manually
- i.e. Swagger
- Must optimize every API call
- Amount of data transferred over the wire matters, a lot
app.get('/users', function (req, res) {
// return list of users
})
app.get('/user/{id}', function (req, res) {
// return a single user with id
})
app.put('/user', function (req, res) {
// update a user
})
REST API example
REST API documentation - Swagger
REST API documentation - Swagger
API design with GraphQL
- GraphQL makes it possible to iteratively implement
- Documentation is baked in
- The consumer decides how much data to pull
- Inherently stateless
- Inherently composable
- Lends itself to functional reactive programming
GraphQL Schema
type Query {
content(
source: Audience!
type: [ContentType!]!
id: Int
zoneId: Int
count: Int = 30
topNews: Boolean = false
dayCount: Int = 365
htmlContent: Boolean = false
): [Article]
...
}
GraphQL Schema
enum Audience {
am # Amharic
bo # Tibetan
en # European english (RFE/RL)
enus # US English
es # Spanish
fa # Farsi
ko # Korean
om # Afaan Oromoo
prs # Dari
pus # Pashto
ru # Russian
tg # Tigrigna
ur # Urdu
vi # Vietnamese
zhcn # Simplified Chinese
}
enum ContentType {
Article
Video
PhotoGallery
Clip
}
enum ArticleVideoRelationship {
SameItem
MainImage
EmbededInContent
}
type Photo {
id: Int
order: Int
url: String!
photoTitle: String
photoDescription: String
tiny: String!
thumb: String!
hero: String!
}
type PhotoGallery {
id: Int!
relType: ArticleVideoRelationship
photoGalleryTitle: String
photoGalleryDescription: String
photo: [Photo]
}
type Name {
first: String!
middle: String
last: String
}
type Author {
name: Name!
email: String
description: String
id: Int!
}
type Image {
imageTitle: String
id: Int!
type: String
url: String!
tiny: String!
thumb: String!
hero: String!
}
type Audio {
audioTitle: String
audioDescription: String
id: Int!
duration: Int
mime: String
url: String
date: String
}
type Video {
videoTitle: String
videoDescription: String
guid: String
relType: ArticleVideoRelationship
id: Int!
width: Int
height: Int
duration: Int
url: String
thumbnail: String
thumbnailTiny: String
thumbnailThumb: String
thumbnailHero: String
}
type RelatedStory {
storyTitle: String!
id: Int!
pubDate: String!
type: ContentType
url: String
twitter: String
thumbnailUrl: String
thumbnailTiny: String
thumbnailThumb: String
thumbnailHero: String
}
type Article {
id: Int!
site: Int
zone: Int
type: ContentType
pubDate: String!
lastUpdated: String
url: String
twitter: String
title: String!
introduction: String!
content: String
authors: [Author]
image: Image
audio: Audio
video: Video
relatedStories: [RelatedStory]
photoGallery: [PhotoGallery]
photo: Photo
}
type Zone {
id: Int!
site: Int
hash: String
type: [ContentType]
broadcast: Boolean
name: String!
}
GraphQL example
export const resolvers = <IResolvers>{
Query: {
content: async (obj: any, args: IContentQueryParams) => {
return await getContent(args)
},
...
},
Article: {
type: (obj: any) => {
return EnumValues.getNameFromValue(ContentType, obj.type)
},
authors: (obj: any) => {
return obj.authors ? wrapAsArray(obj.authors.author) : []
},
...
},
...
}
GraphQL Metrics
- 11 mobile apps in 14 languages
- 9 months
- Kanban
- Multiple redesigns
- One major technology shift
- On time, on budget
- Deployed on serverless infrastructure
- Apps work fast in rural areas
- Two developers
?
💡
- Exposed to a lot of change by
- End-users
- APIs
- Must be able to evolve the architecture without rewrites
- Isolation of components is key
- Custom elements will help
Web App Architecture
Web App Architecture
Angular
app.ts
rootRouter
services
filters
directives
/a: default
/master
/detail
/b/...
/c
childRouter
/d
/e
/f
React
App.js
Presentational
Container
Provider
Router
Component Legend
react-router
react-redux
?
💡
Imperative vs Reactive
- Must define every step of code execution
- Easy to introduce state
- Easy to forget a step
- Difficult to test
Imperative
- OOP
- Functional
Imperative
Reactive
- Programming with asynchronous data streams
- Functional
- Composable
- Stateless
What is a Data Stream?
- Event-driven
- Pub-Sub
- Asynchronous data stream
Event-Driven Data
Event Source
Event Handler
user clicks
window.alert('Are you sure?')
onClick='confirmDelete()'
Publish-Subscribe Pattern
updated data
fetchDetails()
updateCache()
showToastMessage()
Pub/Sub or Event-based use case
Asynchronous Data Stream
Event Source
mouse clicks
filter(x >= 2)
throttle(250ms)
map(list.length)
<li *ngFor="let i in list | async">
window.alert('Are you sure?')
Reactive or Data Stream use case
- Grab a hose
- Spray water into the heater
- Turn on the faucet for hot water
- Send a text to the utility company
- Don't forget to undo your steps, when done
Imperative
- Turn on/off the faucet for hot water
Reactive
Reactive
products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(data => console.log('getProducts: ', JSON.stringify(data))),
shareReplay(),
catchError(this.handleError)
)
Reactive
productsWithCategory$ = combineLatest(
this.products$,
this.productCategoryService.productCategories$
).pipe(
map(([products, categories]) =>
products.map(
p =>
({
...p,
category: categories.find(c => p.categoryId === c.id).name
} as Product) // <-- note the type here!
)
),
shareReplay()
)
Reactive
Data Composition with RxJS | Deborah Kurata
Thinking Reactively: Most Difficult | Mike Pearson
?
💡
What is Router-First Architecture?
- enforce high-level thinking
- ensure consensus on features, before you start coding
- plan on your codebase/team to grow
- little engineering overhead
A way to
- Develop a roadmap and scope
- Design with lazy loading in mind
- Implement a walking-skeleton navigation experience
- Achieve a stateless, data-driven design
- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
?
💡
- Develop a roadmap and scope
- Design with lazy loading in mind
- Implement a walking-skeleton navigation experience
- Achieve a stateless, data-driven design
- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
Let's build a weather app
Lemon
Mart
Lemon
Mart
Stakeholders
😎
😄
😄
😄
Site Map
Site Map
Site Map
1. Develop a Roadmap and Scope
- Get high-level architecture right
- Define the map before getting on the road
- Capture the vision concretely
- Bring tools in only when necessary
- Implement iteratively
- Drive for perfection only after the fundamentals are in place and agreed upon
- Document every artifact you create
Develop a roadmap and scope- Design with lazy loading in mind
- Implement a walking-skeleton navigation experience
- Achieve a stateless, data-driven design
- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
First-Paint Matters, A Lot
- 53% of mobile users* abandon if load times > 3 secs
- Content is consumed mostly on mobile*
- 70% in the US
- 90% in China
- Hybrid client/server-side rendering is hard
*Source: Angular Team, Google Analytics, 2018
Define User Roles
- manager
- inventory
- pos
- unauthorized
Lazy Loading Tips
- Eager-load resources in the background
- Optimize your chunk sizes
- Watch for amounts of assets and compression
- Component level lazy loading will be possible
2. Design with Lazy-Loading in Mind
- First-paint matters a lot
- Lazy loading is low hanging fruit
- Requires user roles to be defined early on
- Very difficult implement lazy-loading after the fact
Develop a roadmap and scopeDesign with lazy loading in mind- Implement a walking-skeleton navigation experience
- Achieve a stateless, data-driven design
- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
- Gather feedback from users
- Workout fundamental workflow and integration issues quickly
- Concrete representation of scope
- Sets the stage for multiple teams to work in tandem
3. Walking-Skeleton Navigation UX
Develop a roadmap and scopeDesign with lazy loading in mindImplement a walking-skeleton navigation experience- Achieve a stateless, data-driven design
- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
RxJS/BehaviorSubject as Data Anchors
readonly currentUser$ = new BehaviorSubject<IUser>(
this.getItem('user') ||
new User()
)
Using RxJS/BehaviorSubject
<div *ngIf="currentUser$ | async as currentUser">
<div>{{currentUser.firstName}}</div>
...
</div>
4. Be Stateless & Data-Driven
- Define observable "data anchors"
- Leverage RxJS features
- Write functional reactive code
- Don't store state in components
- Data across components will be kept in sync
Develop a roadmap and scopeDesign with lazy loading in mindImplement a walking-skeleton navigation experienceAchieve a stateless, data-driven design- Enforce a decoupled component architecture
- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
- @Input bindings
- @Output bindings
- Router orchestration
Ways to decouple
Let the router orchestrate
{
path: 'users',
component: UserManagementComponent,
children: [
{ path: '', component: UserTableComponent, outlet: 'master' },
{
path: 'user',
component: ViewUserComponent,
outlet: 'detail',
resolve: {
user: UserResolve,
},
},
],
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
data: {
expectedRole: Role.Manager,
},
}
5. Be Decoupled
- Every component should be responsible of loading their own data
- Allows for composition of components
- Router enables URL driven composition
- Don't abuse the router
- Ok to design for a parent component to contain multiple hard-coded components
Develop a roadmap and scopeDesign with lazy loading in mindImplement a walking-skeleton navigation experienceAchieve a stateless, data-driven designEnforce a decoupled component architecture- Differentiate between user controls and components
- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
- i.e. Custom Date Input
- Highly coupled, convoluted, complicated code
- Using Angular features no one has ever heard of before
What's a User Control?
- i.e. Form with date inputs
- Code must be easy to read and understand
- Stick to Angular basics
- So code is stable and easy to maintain
What is a component?
- User controls encapsulate complicated code
- User controls can be shipped in libraries
- User controls can be open sourced
- Composable components
- Save time and resources
Reusability
6. User Controls vs Components
- Wire-framing allows to identify reusable elements early on
- Keep user interaction code separate from business logic
- Encapsulate domain specific behavior and share it
Develop a roadmap and scopeDesign with lazy loading in mindImplement a walking-skeleton navigation experienceAchieve a stateless, data-driven designEnforce a decoupled component architectureDifferentiate between user controls and components- Maximize code reuse with ES6/TypeScript
How to implement Router-First?
DRY
- Don't Repeat Yourself
Object-Oriented Design
- Move behavior to classes
- i.e. Hydration, ToJSON, Calculated Properties
- Remain stateless and functional
- Documents shape of data
- Pass abstractions, not concretions
- Separate internal data shape from external shape
- Aim for flat data hierarchy
- Arrays and simple shapes for common objects are okay
- i.e. name object, domain-specific object
Interfaces
- No string literals in code
- No string literals in code
- No string literals in code
Use Enums
7. Use ES6 & TypeScript Features
- Design around abstractions
- Use classes to reuse context specific behavior
- Use interfaces to communicate the shape of internal and external data
- Use enums instead of string literals
- Leverage Angular Validators and Pipes to reuse logic
?
💡
Router-First Metrics
-
10 projects
-
50 developers
-
1 year of code written
-
115 of 1000 components written
-
~25% reduction in package dependencies, variations, and versions
-
Feedback loop cycle reduced from 20 minutes to 1 minute
-
~25% reduction in software bugs reported
-
~25% increase in agile velocit y
Think Router-First
Keep it simple
Master the fundamentals
Be reactive
- What do we develop?
- Why/when/where do we develop it?
- Who develops it?
- How do we develop it?
- Architecture
- API Design
- Web App Design
- Imperative vs Reactive
- Router-first architecture
5W5H
AngularForEnterprise.com
@duluca
linkedin.com/in/duluca
github.com/duluca